Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 0c09c73

Browse files
author
Jon Wayne Parrott
authored
Use "gcloud config config-helper" to read the project ID from the Google Cloud SDK (#147)
1 parent e60c124 commit 0c09c73

6 files changed

Lines changed: 86 additions & 117 deletions

File tree

google/auth/_cloud_sdk.py

Lines changed: 30 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
"""Helpers for reading the Google Cloud SDK's configuration."""
1616

17-
import io
17+
import json
1818
import os
19+
import subprocess
1920

2021
import six
21-
from six.moves import configparser
2222

2323
from google.auth import environment_vars
2424
import google.oauth2.credentials
@@ -33,9 +33,9 @@
3333
# The name of the file in the Cloud SDK config that contains default
3434
# credentials.
3535
_CREDENTIALS_FILENAME = 'application_default_credentials.json'
36-
# The config section and key for the project ID in the cloud SDK config.
37-
_PROJECT_CONFIG_SECTION = 'core'
38-
_PROJECT_CONFIG_KEY = 'project'
36+
# The command to get the Cloud SDK configuration
37+
_CLOUD_SDK_CONFIG_COMMAND = (
38+
'gcloud', 'config', 'config-helper', '--format', 'json')
3939

4040

4141
def get_config_path():
@@ -80,66 +80,6 @@ def get_application_default_credentials_path():
8080
return os.path.join(config_path, _CREDENTIALS_FILENAME)
8181

8282

83-
def _get_active_config(config_path):
84-
"""Gets the active config for the Cloud SDK.
85-
86-
Args:
87-
config_path (str): The Cloud SDK's config path.
88-
89-
Returns:
90-
str: The active configuration name.
91-
"""
92-
active_config_filename = os.path.join(config_path, 'active_config')
93-
94-
if not os.path.isfile(active_config_filename):
95-
return 'default'
96-
97-
with io.open(active_config_filename, 'r', encoding='utf-8') as file_obj:
98-
active_config_name = file_obj.read().strip()
99-
100-
return active_config_name
101-
102-
103-
def _get_config_file(config_path, config_name):
104-
"""Returns the full path to a configuration's config file.
105-
106-
Args:
107-
config_path (str): The Cloud SDK's config path.
108-
config_name (str): The configuration name.
109-
110-
Returns:
111-
str: The config file path.
112-
"""
113-
return os.path.join(
114-
config_path, 'configurations', 'config_{}'.format(config_name))
115-
116-
117-
def get_project_id():
118-
"""Gets the project ID from the Cloud SDK's configuration.
119-
120-
Returns:
121-
Optional[str]: The project ID.
122-
"""
123-
config_path = get_config_path()
124-
active_config = _get_active_config(config_path)
125-
config_file = _get_config_file(config_path, active_config)
126-
127-
if not os.path.isfile(config_file):
128-
return None
129-
130-
config = configparser.RawConfigParser()
131-
132-
try:
133-
config.read(config_file)
134-
135-
if config.has_section(_PROJECT_CONFIG_SECTION):
136-
return config.get(
137-
_PROJECT_CONFIG_SECTION, _PROJECT_CONFIG_KEY)
138-
139-
except configparser.Error:
140-
return None
141-
142-
14383
def load_authorized_user_credentials(info):
14484
"""Loads an authorized user credential.
14585
@@ -166,3 +106,28 @@ def load_authorized_user_credentials(info):
166106
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
167107
client_id=info['client_id'],
168108
client_secret=info['client_secret'])
109+
110+
111+
def get_project_id():
112+
"""Gets the project ID from the Cloud SDK.
113+
114+
Returns:
115+
Optional[str]: The project ID.
116+
"""
117+
118+
try:
119+
output = subprocess.check_output(
120+
_CLOUD_SDK_CONFIG_COMMAND,
121+
stderr=subprocess.STDOUT)
122+
except (subprocess.CalledProcessError, OSError, IOError):
123+
return None
124+
125+
try:
126+
configuration = json.loads(output.decode('utf-8'))
127+
except ValueError:
128+
return None
129+
130+
try:
131+
return configuration['configuration']['properties']['core']['project']
132+
except KeyError:
133+
return None

system_tests/nox.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ def install_cloud_sdk(session):
8585
session.env[CLOUD_SDK_CONFIG_ENV] = str(CLOUD_SDK_ROOT)
8686
# This tells gcloud which Python interpreter to use (always use 2.7)
8787
session.env[CLOUD_SDK_PYTHON_ENV] = CLOUD_SDK_PYTHON
88+
# This set the $PATH for the subprocesses so they can find the gcloud
89+
# executable.
90+
session.env['PATH'] = (
91+
str(CLOUD_SDK_INSTALL_DIR.join('bin')) + os.pathsep +
92+
os.environ['PATH'])
8893

8994
# If gcloud cli executable already exists, just update it.
9095
if py.path.local(GCLOUD).exists():
@@ -130,6 +135,14 @@ def configure_cloud_sdk(
130135
"""
131136
install_cloud_sdk(session)
132137

138+
# Setup the service account as the default user account. This is
139+
# needed for the project ID detection to work. Note that this doesn't
140+
# change the application default credentials file, which is user
141+
# credentials instead of service account credentials sometimes.
142+
session.run(
143+
GCLOUD, 'auth', 'activate-service-account', '--key-file',
144+
SERVICE_ACCOUNT_FILE)
145+
133146
if project:
134147
session.run(GCLOUD, 'config', 'set', 'project', 'example-project')
135148
else:

tests/data/cloud_sdk.cfg

Lines changed: 0 additions & 2 deletions
This file was deleted.

tests/data/cloud_sdk_config.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"configuration": {
3+
"active_configuration": "default",
4+
"properties": {
5+
"core": {
6+
"account": "user@example.com",
7+
"disable_usage_reporting": "False",
8+
"project": "example-project"
9+
}
10+
}
11+
},
12+
"credential": {
13+
"access_token": "don't use me",
14+
"token_expiry": "2017-03-23T23:09:49Z"
15+
},
16+
"sentinels": {
17+
"config_sentinel": "/Users/example/.config/gcloud/config_sentinel"
18+
}
19+
}

tests/test__cloud_sdk.py

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import io
1516
import json
1617
import os
18+
import subprocess
1719

1820
import mock
19-
import py
2021
import pytest
2122

2223
from google.auth import _cloud_sdk
@@ -27,76 +28,52 @@
2728
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
2829
AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, 'authorized_user.json')
2930

30-
with open(AUTHORIZED_USER_FILE) as fh:
31+
with io.open(AUTHORIZED_USER_FILE) as fh:
3132
AUTHORIZED_USER_FILE_DATA = json.load(fh)
3233

3334
SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, 'service_account.json')
3435

35-
with open(SERVICE_ACCOUNT_FILE) as fh:
36+
with io.open(SERVICE_ACCOUNT_FILE) as fh:
3637
SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
3738

38-
with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh:
39-
CLOUD_SDK_CONFIG_DATA = fh.read()
39+
with io.open(os.path.join(DATA_DIR, 'cloud_sdk_config.json'), 'rb') as fh:
40+
CLOUD_SDK_CONFIG_FILE_DATA = fh.read()
4041

41-
CONFIG_PATH_PATCH = mock.patch(
42-
'google.auth._cloud_sdk.get_config_path', autospec=True)
43-
44-
45-
@pytest.fixture
46-
def config_dir(tmpdir):
47-
config_dir = tmpdir.join(
48-
'.config', _cloud_sdk._CONFIG_DIRECTORY)
49-
50-
with CONFIG_PATH_PATCH as mock_get_config_dir:
51-
mock_get_config_dir.return_value = str(config_dir)
52-
yield config_dir
5342

54-
55-
@pytest.fixture
56-
def config_file(config_dir):
57-
config_file = py.path.local(_cloud_sdk._get_config_file(
58-
str(config_dir), 'default'))
59-
yield config_file
60-
61-
62-
def test_get_project_id(config_file):
63-
config_file.write(CLOUD_SDK_CONFIG_DATA, ensure=True)
43+
@mock.patch(
44+
'subprocess.check_output', autospec=True,
45+
return_value=CLOUD_SDK_CONFIG_FILE_DATA)
46+
def test_get_project_id(check_output_mock):
6447
project_id = _cloud_sdk.get_project_id()
6548
assert project_id == 'example-project'
6649

6750

68-
def test_get_project_id_non_existent(config_file):
51+
@mock.patch(
52+
'subprocess.check_output', autospec=True,
53+
side_effect=subprocess.CalledProcessError(-1, None))
54+
def test_get_project_id_call_error(check_output_mock):
6955
project_id = _cloud_sdk.get_project_id()
7056
assert project_id is None
7157

7258

73-
def test_get_project_id_bad_file(config_file):
74-
config_file.write('<<<badconfig', ensure=True)
59+
@mock.patch(
60+
'subprocess.check_output', autospec=True,
61+
return_value=b'I am some bad json')
62+
def test_get_project_id_bad_json(check_output_mock):
7563
project_id = _cloud_sdk.get_project_id()
7664
assert project_id is None
7765

7866

79-
def test_get_project_id_no_section(config_file):
80-
config_file.write('[section]', ensure=True)
67+
@mock.patch(
68+
'subprocess.check_output', autospec=True,
69+
return_value=b'{}')
70+
def test_get_project_id_missing_value(check_output_mock):
8171
project_id = _cloud_sdk.get_project_id()
8272
assert project_id is None
8373

8474

85-
def test_get_project_id_non_default_config(config_dir):
86-
active_config = config_dir.join('active_config')
87-
test_config = py.path.local(_cloud_sdk._get_config_file(
88-
str(config_dir), 'test'))
89-
90-
# Create an active config file that points to the 'test' config.
91-
active_config.write('test', ensure=True)
92-
test_config.write(CLOUD_SDK_CONFIG_DATA, ensure=True)
93-
94-
project_id = _cloud_sdk.get_project_id()
95-
96-
assert project_id == 'example-project'
97-
98-
99-
@CONFIG_PATH_PATCH
75+
@mock.patch(
76+
'google.auth._cloud_sdk.get_config_path', autospec=True)
10077
def test_get_application_default_credentials_path(mock_get_config_dir):
10178
config_path = 'config_path'
10279
mock_get_config_dir.return_value = config_path

tests/test__default.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
with open(SERVICE_ACCOUNT_FILE) as fh:
3939
SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
4040

41-
with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh:
42-
CLOUD_SDK_CONFIG_DATA = fh.read()
43-
4441
LOAD_FILE_PATCH = mock.patch(
4542
'google.auth._default._load_credentials_from_file', return_value=(
4643
mock.sentinel.credentials, mock.sentinel.project_id), autospec=True)

0 commit comments

Comments
 (0)