Use "gcloud config config-helper" to read the project ID from the Google Cloud SDK (#147)
diff --git a/google/auth/_cloud_sdk.py b/google/auth/_cloud_sdk.py
index 1e851c8..428b612 100644
--- a/google/auth/_cloud_sdk.py
+++ b/google/auth/_cloud_sdk.py
@@ -14,11 +14,11 @@
"""Helpers for reading the Google Cloud SDK's configuration."""
-import io
+import json
import os
+import subprocess
import six
-from six.moves import configparser
from google.auth import environment_vars
import google.oauth2.credentials
@@ -33,9 +33,9 @@
# The name of the file in the Cloud SDK config that contains default
# credentials.
_CREDENTIALS_FILENAME = 'application_default_credentials.json'
-# The config section and key for the project ID in the cloud SDK config.
-_PROJECT_CONFIG_SECTION = 'core'
-_PROJECT_CONFIG_KEY = 'project'
+# The command to get the Cloud SDK configuration
+_CLOUD_SDK_CONFIG_COMMAND = (
+ 'gcloud', 'config', 'config-helper', '--format', 'json')
def get_config_path():
@@ -80,66 +80,6 @@
return os.path.join(config_path, _CREDENTIALS_FILENAME)
-def _get_active_config(config_path):
- """Gets the active config for the Cloud SDK.
-
- Args:
- config_path (str): The Cloud SDK's config path.
-
- Returns:
- str: The active configuration name.
- """
- active_config_filename = os.path.join(config_path, 'active_config')
-
- if not os.path.isfile(active_config_filename):
- return 'default'
-
- with io.open(active_config_filename, 'r', encoding='utf-8') as file_obj:
- active_config_name = file_obj.read().strip()
-
- return active_config_name
-
-
-def _get_config_file(config_path, config_name):
- """Returns the full path to a configuration's config file.
-
- Args:
- config_path (str): The Cloud SDK's config path.
- config_name (str): The configuration name.
-
- Returns:
- str: The config file path.
- """
- return os.path.join(
- config_path, 'configurations', 'config_{}'.format(config_name))
-
-
-def get_project_id():
- """Gets the project ID from the Cloud SDK's configuration.
-
- Returns:
- Optional[str]: The project ID.
- """
- config_path = get_config_path()
- active_config = _get_active_config(config_path)
- config_file = _get_config_file(config_path, active_config)
-
- if not os.path.isfile(config_file):
- return None
-
- config = configparser.RawConfigParser()
-
- try:
- config.read(config_file)
-
- if config.has_section(_PROJECT_CONFIG_SECTION):
- return config.get(
- _PROJECT_CONFIG_SECTION, _PROJECT_CONFIG_KEY)
-
- except configparser.Error:
- return None
-
-
def load_authorized_user_credentials(info):
"""Loads an authorized user credential.
@@ -166,3 +106,28 @@
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
client_id=info['client_id'],
client_secret=info['client_secret'])
+
+
+def get_project_id():
+ """Gets the project ID from the Cloud SDK.
+
+ Returns:
+ Optional[str]: The project ID.
+ """
+
+ try:
+ output = subprocess.check_output(
+ _CLOUD_SDK_CONFIG_COMMAND,
+ stderr=subprocess.STDOUT)
+ except (subprocess.CalledProcessError, OSError, IOError):
+ return None
+
+ try:
+ configuration = json.loads(output.decode('utf-8'))
+ except ValueError:
+ return None
+
+ try:
+ return configuration['configuration']['properties']['core']['project']
+ except KeyError:
+ return None
diff --git a/system_tests/nox.py b/system_tests/nox.py
index 0d1116d..fa0422a 100644
--- a/system_tests/nox.py
+++ b/system_tests/nox.py
@@ -85,6 +85,11 @@
session.env[CLOUD_SDK_CONFIG_ENV] = str(CLOUD_SDK_ROOT)
# This tells gcloud which Python interpreter to use (always use 2.7)
session.env[CLOUD_SDK_PYTHON_ENV] = CLOUD_SDK_PYTHON
+ # This set the $PATH for the subprocesses so they can find the gcloud
+ # executable.
+ session.env['PATH'] = (
+ str(CLOUD_SDK_INSTALL_DIR.join('bin')) + os.pathsep +
+ os.environ['PATH'])
# If gcloud cli executable already exists, just update it.
if py.path.local(GCLOUD).exists():
@@ -130,6 +135,14 @@
"""
install_cloud_sdk(session)
+ # Setup the service account as the default user account. This is
+ # needed for the project ID detection to work. Note that this doesn't
+ # change the application default credentials file, which is user
+ # credentials instead of service account credentials sometimes.
+ session.run(
+ GCLOUD, 'auth', 'activate-service-account', '--key-file',
+ SERVICE_ACCOUNT_FILE)
+
if project:
session.run(GCLOUD, 'config', 'set', 'project', 'example-project')
else:
diff --git a/tests/data/cloud_sdk.cfg b/tests/data/cloud_sdk.cfg
deleted file mode 100644
index 089aac5..0000000
--- a/tests/data/cloud_sdk.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[core]
-project = example-project
diff --git a/tests/data/cloud_sdk_config.json b/tests/data/cloud_sdk_config.json
new file mode 100644
index 0000000..a5fe4a9
--- /dev/null
+++ b/tests/data/cloud_sdk_config.json
@@ -0,0 +1,19 @@
+{
+ "configuration": {
+ "active_configuration": "default",
+ "properties": {
+ "core": {
+ "account": "user@example.com",
+ "disable_usage_reporting": "False",
+ "project": "example-project"
+ }
+ }
+ },
+ "credential": {
+ "access_token": "don't use me",
+ "token_expiry": "2017-03-23T23:09:49Z"
+ },
+ "sentinels": {
+ "config_sentinel": "/Users/example/.config/gcloud/config_sentinel"
+ }
+}
diff --git a/tests/test__cloud_sdk.py b/tests/test__cloud_sdk.py
index ba72072..482a7ed 100644
--- a/tests/test__cloud_sdk.py
+++ b/tests/test__cloud_sdk.py
@@ -12,11 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import io
import json
import os
+import subprocess
import mock
-import py
import pytest
from google.auth import _cloud_sdk
@@ -27,76 +28,52 @@
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, 'authorized_user.json')
-with open(AUTHORIZED_USER_FILE) as fh:
+with io.open(AUTHORIZED_USER_FILE) as fh:
AUTHORIZED_USER_FILE_DATA = json.load(fh)
SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, 'service_account.json')
-with open(SERVICE_ACCOUNT_FILE) as fh:
+with io.open(SERVICE_ACCOUNT_FILE) as fh:
SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
-with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh:
- CLOUD_SDK_CONFIG_DATA = fh.read()
+with io.open(os.path.join(DATA_DIR, 'cloud_sdk_config.json'), 'rb') as fh:
+ CLOUD_SDK_CONFIG_FILE_DATA = fh.read()
-CONFIG_PATH_PATCH = mock.patch(
+
+@mock.patch(
+ 'subprocess.check_output', autospec=True,
+ return_value=CLOUD_SDK_CONFIG_FILE_DATA)
+def test_get_project_id(check_output_mock):
+ project_id = _cloud_sdk.get_project_id()
+ assert project_id == 'example-project'
+
+
+@mock.patch(
+ 'subprocess.check_output', autospec=True,
+ side_effect=subprocess.CalledProcessError(-1, None))
+def test_get_project_id_call_error(check_output_mock):
+ project_id = _cloud_sdk.get_project_id()
+ assert project_id is None
+
+
+@mock.patch(
+ 'subprocess.check_output', autospec=True,
+ return_value=b'I am some bad json')
+def test_get_project_id_bad_json(check_output_mock):
+ project_id = _cloud_sdk.get_project_id()
+ assert project_id is None
+
+
+@mock.patch(
+ 'subprocess.check_output', autospec=True,
+ return_value=b'{}')
+def test_get_project_id_missing_value(check_output_mock):
+ project_id = _cloud_sdk.get_project_id()
+ assert project_id is None
+
+
+@mock.patch(
'google.auth._cloud_sdk.get_config_path', autospec=True)
-
-
-@pytest.fixture
-def config_dir(tmpdir):
- config_dir = tmpdir.join(
- '.config', _cloud_sdk._CONFIG_DIRECTORY)
-
- with CONFIG_PATH_PATCH as mock_get_config_dir:
- mock_get_config_dir.return_value = str(config_dir)
- yield config_dir
-
-
-@pytest.fixture
-def config_file(config_dir):
- config_file = py.path.local(_cloud_sdk._get_config_file(
- str(config_dir), 'default'))
- yield config_file
-
-
-def test_get_project_id(config_file):
- config_file.write(CLOUD_SDK_CONFIG_DATA, ensure=True)
- project_id = _cloud_sdk.get_project_id()
- assert project_id == 'example-project'
-
-
-def test_get_project_id_non_existent(config_file):
- project_id = _cloud_sdk.get_project_id()
- assert project_id is None
-
-
-def test_get_project_id_bad_file(config_file):
- config_file.write('<<<badconfig', ensure=True)
- project_id = _cloud_sdk.get_project_id()
- assert project_id is None
-
-
-def test_get_project_id_no_section(config_file):
- config_file.write('[section]', ensure=True)
- project_id = _cloud_sdk.get_project_id()
- assert project_id is None
-
-
-def test_get_project_id_non_default_config(config_dir):
- active_config = config_dir.join('active_config')
- test_config = py.path.local(_cloud_sdk._get_config_file(
- str(config_dir), 'test'))
-
- # Create an active config file that points to the 'test' config.
- active_config.write('test', ensure=True)
- test_config.write(CLOUD_SDK_CONFIG_DATA, ensure=True)
-
- project_id = _cloud_sdk.get_project_id()
-
- assert project_id == 'example-project'
-
-
-@CONFIG_PATH_PATCH
def test_get_application_default_credentials_path(mock_get_config_dir):
config_path = 'config_path'
mock_get_config_dir.return_value = config_path
diff --git a/tests/test__default.py b/tests/test__default.py
index a317e0a..001a19c 100644
--- a/tests/test__default.py
+++ b/tests/test__default.py
@@ -38,9 +38,6 @@
with open(SERVICE_ACCOUNT_FILE) as fh:
SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
-with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh:
- CLOUD_SDK_CONFIG_DATA = fh.read()
-
LOAD_FILE_PATCH = mock.patch(
'google.auth._default._load_credentials_from_file', return_value=(
mock.sentinel.credentials, mock.sentinel.project_id), autospec=True)