Skip to content

Commit

Permalink
Use "gcloud config config-helper" to read the project ID from the Goo…
Browse files Browse the repository at this point in the history
…gle Cloud SDK (#147)
  • Loading branch information
Jon Wayne Parrott authored Mar 24, 2017
1 parent e60c124 commit 0c09c73
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 117 deletions.
95 changes: 30 additions & 65 deletions google/auth/_cloud_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -80,66 +80,6 @@ def get_application_default_credentials_path():
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.
Expand All @@ -166,3 +106,28 @@ def load_authorized_user_credentials(info):
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
13 changes: 13 additions & 0 deletions system_tests/nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def install_cloud_sdk(session):
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():
Expand Down Expand Up @@ -130,6 +135,14 @@ def configure_cloud_sdk(
"""
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:
Expand Down
2 changes: 0 additions & 2 deletions tests/data/cloud_sdk.cfg

This file was deleted.

19 changes: 19 additions & 0 deletions tests/data/cloud_sdk_config.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
71 changes: 24 additions & 47 deletions tests/test__cloud_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
'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)
@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'


def test_get_project_id_non_existent(config_file):
@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


def test_get_project_id_bad_file(config_file):
config_file.write('<<<badconfig', ensure=True)
@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


def test_get_project_id_no_section(config_file):
config_file.write('[section]', ensure=True)
@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


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
@mock.patch(
'google.auth._cloud_sdk.get_config_path', autospec=True)
def test_get_application_default_credentials_path(mock_get_config_dir):
config_path = 'config_path'
mock_get_config_dir.return_value = config_path
Expand Down
3 changes: 0 additions & 3 deletions tests/test__default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 0c09c73

Please sign in to comment.