From a437715275974ddd5607ac56e1782cb0006f42c7 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Mon, 23 May 2016 00:09:55 -0400 Subject: [PATCH 01/13] Add project ID from credentials. --- gcloud/_helpers.py | 21 +++++++++++++++++++++ gcloud/test__helpers.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 5853785adfaf..c27c29e52e18 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -18,6 +18,7 @@ import calendar import datetime +import json import os import re import socket @@ -159,6 +160,23 @@ def _app_engine_id(): return app_identity.get_application_id() +def _file_project_id(): + """Gets the project id from the credentials file if one is available. + + :rtype: string or ``NoneType`` + :returns: Project-ID from JSON credentials file if value exists, + else ``None``. + """ + credentials_file_path = os.getenv('GOOGLE_APPLICATION_CREDENTIALS') + if credentials_file_path: + credentials_file = open(credentials_file_path, 'r') + credentials_data = credentials_file.read() + credentials = json.loads(credentials_data) + return credentials.get('project_id') + else: + return None + + def _compute_engine_id(): """Gets the Compute Engine project ID if it can be inferred. @@ -216,6 +234,9 @@ def _determine_default_project(project=None): if project is None: project = _get_production_project() + if project is None: + project = _file_project_id() + if project is None: project = _app_engine_id() diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index dffd8f0cc38c..ad9e1a722027 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -148,6 +148,38 @@ def test_value_set(self): self.assertEqual(dataset_id, APP_ENGINE_ID) +class Test__get_credentials_file_project_id(unittest2.TestCase): + + def _callFUT(self): + from gcloud._helpers import _file_project_id + return _file_project_id() + + def setUp(self): + import os + + self.old_env = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS', '') + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = '' + + def tearDown(self): + import os + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.old_env + + def test_success(self): + import os + import tempfile + with tempfile.NamedTemporaryFile() as credential_file: + credential_file.write('{"project_id": "test-project-id"}') + credential_file.seek(0) + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = credential_file.name + + self.assertEqual('test-project-id', self._callFUT()) + + def test_failure(self): + import os + del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] + self.assertEqual(None, self._callFUT()) + + class Test__compute_engine_id(unittest2.TestCase): def _callFUT(self): From 67a4b5468f124d8ba6f8487ed4c5087727ed22a6 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Mon, 23 May 2016 09:17:57 -0400 Subject: [PATCH 02/13] Update docs mentioning default project. --- docs/gcloud-auth.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/gcloud-auth.rst b/docs/gcloud-auth.rst index c5b10b8a6763..11ea7c93f022 100644 --- a/docs/gcloud-auth.rst +++ b/docs/gcloud-auth.rst @@ -39,6 +39,10 @@ Overview $ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json" + The default project ID will be the project that keyfile.json was genereated + from. This can be overridded by setting the ``GCLOUD_PROJECT`` environment + variable. + .. _service account: https://cloud.google.com/storage/docs/authentication#generating-a-private-key Client-Provided Authentication From 078220c1df6cdcbb23f22a5af3cd128dae64d883 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Mon, 23 May 2016 12:39:31 -0400 Subject: [PATCH 03/13] Add context file handling, fix no env test. --- docs/gcloud-auth.rst | 4 ---- gcloud/_helpers.py | 13 +++++++++---- gcloud/test__helpers.py | 17 ++++++++++------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/gcloud-auth.rst b/docs/gcloud-auth.rst index 11ea7c93f022..c5b10b8a6763 100644 --- a/docs/gcloud-auth.rst +++ b/docs/gcloud-auth.rst @@ -39,10 +39,6 @@ Overview $ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/keyfile.json" - The default project ID will be the project that keyfile.json was genereated - from. This can be overridded by setting the ``GCLOUD_PROJECT`` environment - variable. - .. _service account: https://cloud.google.com/storage/docs/authentication#generating-a-private-key Client-Provided Authentication diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index c27c29e52e18..e62c0ba7335e 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -169,10 +169,10 @@ def _file_project_id(): """ credentials_file_path = os.getenv('GOOGLE_APPLICATION_CREDENTIALS') if credentials_file_path: - credentials_file = open(credentials_file_path, 'r') - credentials_data = credentials_file.read() - credentials = json.loads(credentials_data) - return credentials.get('project_id') + with open(credentials_file_path, 'r') as credentials_file: + credentials_data = credentials_file.read() + credentials = json.loads(credentials_data) + return credentials.get('project_id') else: return None @@ -222,6 +222,8 @@ def _determine_default_project(project=None): implicit environments are: * GCLOUD_PROJECT environment variable + * GOOGLE_APPLICATION_CREDENTIALS JSON file + * Get from oauth defaults * Google App Engine application ID * Google Compute Engine project ID (from metadata server) @@ -237,6 +239,9 @@ def _determine_default_project(project=None): if project is None: project = _file_project_id() + # if project is None: + # print oauth2client.get_application_default() + if project is None: project = _app_engine_id() diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index ad9e1a722027..255bf5fe7423 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -156,27 +156,30 @@ def _callFUT(self): def setUp(self): import os - - self.old_env = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS', '') - os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = '' + self.old_env = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS') def tearDown(self): import os - os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.old_env + if self.old_env: + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.old_env + elif (not self.old_env and + 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ): + del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] def test_success(self): import os import tempfile - with tempfile.NamedTemporaryFile() as credential_file: + with tempfile.NamedTemporaryFile(mode='w') as credential_file: credential_file.write('{"project_id": "test-project-id"}') credential_file.seek(0) os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = credential_file.name self.assertEqual('test-project-id', self._callFUT()) - def test_failure(self): + def test_no_environment(self): import os - del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] + if 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ: + del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] self.assertEqual(None, self._callFUT()) From 2fdbfa40bbab63dc08b48e470272b3c213dd5030 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Mon, 23 May 2016 12:57:30 -0400 Subject: [PATCH 04/13] add gcloud-config.rst --- docs/gcloud-config.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/gcloud-config.rst diff --git a/docs/gcloud-config.rst b/docs/gcloud-config.rst new file mode 100644 index 000000000000..e69de29bb2d1 From ceeaddfe07d3079d830166d7e2a1384dc2cdf15f Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Tue, 24 May 2016 18:37:55 -0400 Subject: [PATCH 05/13] Add Docs and Service account project ID --- docs/gcloud-config.rst | 55 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + gcloud/_helpers.py | 53 +++++++++++++++++++++++++-------------- gcloud/test__helpers.py | 52 +++++++++++++++++++++++++++++++++++--- 4 files changed, 139 insertions(+), 22 deletions(-) diff --git a/docs/gcloud-config.rst b/docs/gcloud-config.rst index e69de29bb2d1..43b06e882ffa 100644 --- a/docs/gcloud-config.rst +++ b/docs/gcloud-config.rst @@ -0,0 +1,55 @@ +Configuration +************* + +Overview +======== + +- Use service :class:`Client ` objects to configure + your applications. + + For example: + + .. code-block:: python + + >>> from gcloud import bigquery + >>> client = bigquery.Client() + +- :class:`Client ` objects hold both a ``project`` + and an authenticated connection to a service. + +- The authentication credentials can be implicitly determined from the + environment or directly via + :meth:`from_service_account_json ` + and + :meth:`from_service_account_p12 `. + +- Logging in with the `Google Cloud SDK`_ will automatically configure a JSON + key file with your default project ID and credentials. + Setting the ``GOOGLE_APPLICATION_CREDENTIALS`` and ``GCLOUD_PROJECT`` + environment variables will override the automatically configured credentials. + +- You can change your default project ID to ``my-new-default-project`` with + ``gcloud`` command line tool. + + .. code-block:: bash + + $ gcloud config set project my-new-default-project + +.. _Google Cloud SDK: http://cloud.google.com/sdk + +- You can override the credentials inferred from the environment by passing + explicit ``credentials`` to one of the alternative ``classmethod`` factories, + :meth:`gcloud.client.Client.from_service_account_json`: + + .. code-block:: python + + >>> from gcloud import bigquery + >>> client = bigquery.Client.from_service_account_json('/path/to/creds.json') + + or :meth:`gcloud.client.Client.from_service_account_p12`: + + .. code-block:: python + + >>> from gcloud import bigquery + >>> client = bigquery.Client.from_service_account_p12( + ... '/path/to/creds.p12', 'jrandom@example.com') diff --git a/docs/index.rst b/docs/index.rst index c026d3bf8887..df0aa0ea9980 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,7 @@ :caption: gcloud gcloud-api + gcloud-config gcloud-auth .. toctree:: diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index e62c0ba7335e..efa92b7a0465 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -22,6 +22,7 @@ import os import re import socket +import subprocess import sys from threading import local as Local @@ -130,13 +131,13 @@ def _ensure_tuple_or_list(arg_name, tuple_or_list): This effectively reduces the iterable types allowed to a very short whitelist: list and tuple. - :type arg_name: string + :type arg_name: str :param arg_name: Name of argument to use in error message. - :type tuple_or_list: sequence of string + :type tuple_or_list: sequence of str :param tuple_or_list: Sequence to be verified. - :rtype: list of string + :rtype: list of str :returns: The ``tuple_or_list`` passed in cast to a ``list``. :raises: class:`TypeError` if the ``tuple_or_list`` is not a tuple or list. @@ -150,7 +151,7 @@ def _ensure_tuple_or_list(arg_name, tuple_or_list): def _app_engine_id(): """Gets the App Engine application ID if it can be inferred. - :rtype: string or ``NoneType`` + :rtype: str or ``NoneType`` :returns: App Engine application ID if running in App Engine, else ``None``. """ @@ -163,7 +164,7 @@ def _app_engine_id(): def _file_project_id(): """Gets the project id from the credentials file if one is available. - :rtype: string or ``NoneType`` + :rtype: str or ``NoneType`` :returns: Project-ID from JSON credentials file if value exists, else ``None``. """ @@ -177,6 +178,22 @@ def _file_project_id(): return None +def _default_service_project_id(): + """Retrieves the project ID from the gcloud command line tool. + + :rtype: str or ``NoneType`` + :returns: Project-ID from ``gcloud info`` else ``None`` + """ + gcloud_project_conf = subprocess.check_output(['gcloud', 'info']) + gcloud_project_conf = gcloud_project_conf.split('\n') + + for key in gcloud_project_conf: + if key.startswith('Project:'): + return key[10:-1] + + return None + + def _compute_engine_id(): """Gets the Compute Engine project ID if it can be inferred. @@ -190,7 +207,7 @@ def _compute_engine_id(): See https://github.com/google/oauth2client/issues/93 for context about DNS latency. - :rtype: string or ``NoneType`` + :rtype: str or ``NoneType`` :returns: Compute Engine project ID if the metadata service is available, else ``None``. """ @@ -223,14 +240,14 @@ def _determine_default_project(project=None): * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file - * Get from oauth defaults + * Get from `gcloud auth login` defaults * Google App Engine application ID * Google Compute Engine project ID (from metadata server) - :type project: string + :type project: str :param project: Optional. The project name to use as default. - :rtype: string or ``NoneType`` + :rtype: str or ``NoneType`` :returns: Default project if it can be determined. """ if project is None: @@ -239,8 +256,8 @@ def _determine_default_project(project=None): if project is None: project = _file_project_id() - # if project is None: - # print oauth2client.get_application_default() + if project is None: + project = _default_service_project_id() if project is None: project = _app_engine_id() @@ -257,7 +274,7 @@ def _millis(when): :type when: :class:`datetime.datetime` :param when: the datetime to convert - :rtype: integer + :rtype: int :returns: milliseconds since epoch for ``when`` """ micros = _microseconds_from_datetime(when) @@ -282,7 +299,7 @@ def _microseconds_from_datetime(value): :type value: :class:`datetime.datetime` :param value: The timestamp to convert. - :rtype: integer + :rtype: int :returns: The timestamp, in microseconds. """ if not value.tzinfo: @@ -299,7 +316,7 @@ def _millis_from_datetime(value): :type value: :class:`datetime.datetime`, or None :param value: the timestamp - :rtype: integer, or ``NoneType`` + :rtype: int, or ``NoneType`` :returns: the timestamp, in milliseconds, or None """ if value is not None: @@ -456,20 +473,20 @@ def _datetime_to_pb_timestamp(when): def _name_from_project_path(path, project, template): """Validate a URI path and get the leaf object's name. - :type path: string + :type path: str :param path: URI path containing the name. - :type project: string or NoneType + :type project: str or NoneType :param project: The project associated with the request. It is included for validation purposes. If passed as None, disables validation. - :type template: string + :type template: str :param template: Template regex describing the expected form of the path. The regex must have two named groups, 'project' and 'name'. - :rtype: string + :rtype: str :returns: Name parsed from ``path``. :raises: :class:`ValueError` if the ``path`` is ill-formed or if the project from the ``path`` does not agree with the diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index 255bf5fe7423..e6bb3147be10 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -183,6 +183,37 @@ def test_no_environment(self): self.assertEqual(None, self._callFUT()) +class Test__get_default_service_project_id(unittest2.TestCase): + + def callFUT(self, project_result=''): + from gcloud._helpers import _default_service_project_id + import subprocess + self.check_output_called_with = [] + + def check_output_mock(called_with=None): + self.check_output_called_with = called_with + return project_result + + from gcloud._testing import _Monkey + with _Monkey(subprocess, check_output=check_output_mock): + return _default_service_project_id(), self.check_output_called_with + + def test_read_from_cli_info(self): + project_id, called_with = self.callFUT('Project: [test-project-id]') + self.assertEqual('test-project-id', project_id) + self.assertEqual(['gcloud', 'info'], called_with) + + def test_cli_info_not_set(self): + project_id, called_with = self.callFUT() + self.assertEqual(None, project_id) + self.assertEqual(['gcloud', 'info'], called_with) + + def test_info_value_not_present(self): + project_id, called_with = self.callFUT('Active Configuration') + self.assertEqual(None, project_id) + self.assertEqual(['gcloud', 'info'], called_with) + + class Test__compute_engine_id(unittest2.TestCase): def _callFUT(self): @@ -254,7 +285,7 @@ def _callFUT(self, project=None): return _determine_default_project(project=project) def _determine_default_helper(self, prod=None, gae=None, gce=None, - project=None): + file_id=None, srv_id=None, project=None): from gcloud._testing import _Monkey from gcloud import _helpers @@ -264,6 +295,14 @@ def prod_mock(): _callers.append('prod_mock') return prod + def file_id_mock(): + _callers.append('file_id_mock') + return file_id + + def srv_id_mock(): + _callers.append('srv_id_mock') + return srv_id + def gae_mock(): _callers.append('gae_mock') return gae @@ -274,6 +313,8 @@ def gce_mock(): patched_methods = { '_get_production_project': prod_mock, + '_file_project_id': file_id_mock, + '_default_service_project_id': srv_id_mock, '_app_engine_id': gae_mock, '_compute_engine_id': gce_mock, } @@ -286,7 +327,8 @@ def gce_mock(): def test_no_value(self): project, callers = self._determine_default_helper() self.assertEqual(project, None) - self.assertEqual(callers, ['prod_mock', 'gae_mock', 'gce_mock']) + self.assertEqual(callers, ['prod_mock', 'file_id_mock', 'srv_id_mock', + 'gae_mock', 'gce_mock']) def test_explicit(self): PROJECT = object() @@ -304,13 +346,15 @@ def test_gae(self): PROJECT = object() project, callers = self._determine_default_helper(gae=PROJECT) self.assertEqual(project, PROJECT) - self.assertEqual(callers, ['prod_mock', 'gae_mock']) + self.assertEqual(callers, ['prod_mock', 'file_id_mock', + 'srv_id_mock', 'gae_mock']) def test_gce(self): PROJECT = object() project, callers = self._determine_default_helper(gce=PROJECT) self.assertEqual(project, PROJECT) - self.assertEqual(callers, ['prod_mock', 'gae_mock', 'gce_mock']) + self.assertEqual(callers, ['prod_mock', 'file_id_mock', 'srv_id_mock', + 'gae_mock', 'gce_mock']) class Test__millis(unittest2.TestCase): From efdfa928779d1022c8f033b56c940b188c1c8e3c Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Wed, 25 May 2016 09:40:49 -0400 Subject: [PATCH 06/13] Switch to Popen for py26 --- gcloud/_helpers.py | 4 +++- gcloud/test__helpers.py | 31 +++++++++---------------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index efa92b7a0465..0a3cb4cd3000 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -184,7 +184,9 @@ def _default_service_project_id(): :rtype: str or ``NoneType`` :returns: Project-ID from ``gcloud info`` else ``None`` """ - gcloud_project_conf = subprocess.check_output(['gcloud', 'info']) + command = subprocess.Popen(['gcloud info'], + stdout=subprocess.PIPE, shell=True) + gcloud_project_conf = command.communicate()[0].decode('utf-8') gcloud_project_conf = gcloud_project_conf.split('\n') for key in gcloud_project_conf: diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index e6bb3147be10..1f9b3c9c14ec 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -160,10 +160,8 @@ def setUp(self): def tearDown(self): import os - if self.old_env: - os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.old_env - elif (not self.old_env and - 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ): + if (not self.old_env and + 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ): del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] def test_success(self): @@ -177,9 +175,6 @@ def test_success(self): self.assertEqual('test-project-id', self._callFUT()) def test_no_environment(self): - import os - if 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ: - del os.environ['GOOGLE_APPLICATION_CREDENTIALS'] self.assertEqual(None, self._callFUT()) @@ -188,30 +183,22 @@ class Test__get_default_service_project_id(unittest2.TestCase): def callFUT(self, project_result=''): from gcloud._helpers import _default_service_project_id import subprocess - self.check_output_called_with = [] - def check_output_mock(called_with=None): - self.check_output_called_with = called_with - return project_result + def popen_communicate_mock(popen_object): + popen_object.kill() + return (project_result,) from gcloud._testing import _Monkey - with _Monkey(subprocess, check_output=check_output_mock): - return _default_service_project_id(), self.check_output_called_with + with _Monkey(subprocess.Popen, communicate=popen_communicate_mock): + return _default_service_project_id() def test_read_from_cli_info(self): - project_id, called_with = self.callFUT('Project: [test-project-id]') + project_id = self.callFUT(b'Project: [test-project-id]') self.assertEqual('test-project-id', project_id) - self.assertEqual(['gcloud', 'info'], called_with) - - def test_cli_info_not_set(self): - project_id, called_with = self.callFUT() - self.assertEqual(None, project_id) - self.assertEqual(['gcloud', 'info'], called_with) def test_info_value_not_present(self): - project_id, called_with = self.callFUT('Active Configuration') + project_id = self.callFUT(b'Active Configuration') self.assertEqual(None, project_id) - self.assertEqual(['gcloud', 'info'], called_with) class Test__compute_engine_id(unittest2.TestCase): From 88240159f4e982afa20886441ca3f890ab617a98 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Wed, 25 May 2016 13:20:45 -0400 Subject: [PATCH 07/13] Capture gcloud info output --- gcloud/_helpers.py | 9 +++++---- gcloud/test__helpers.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 0a3cb4cd3000..b04d9abf736a 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -184,14 +184,15 @@ def _default_service_project_id(): :rtype: str or ``NoneType`` :returns: Project-ID from ``gcloud info`` else ``None`` """ - command = subprocess.Popen(['gcloud info'], - stdout=subprocess.PIPE, shell=True) + command = subprocess.Popen(['gcloud', 'config', 'list', 'project'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True) gcloud_project_conf = command.communicate()[0].decode('utf-8') gcloud_project_conf = gcloud_project_conf.split('\n') for key in gcloud_project_conf: - if key.startswith('Project:'): - return key[10:-1] + if key.startswith('project = '): + return key[10:] return None diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index 1f9b3c9c14ec..d0724c64befa 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -193,7 +193,7 @@ def popen_communicate_mock(popen_object): return _default_service_project_id() def test_read_from_cli_info(self): - project_id = self.callFUT(b'Project: [test-project-id]') + project_id = self.callFUT(b'project = test-project-id') self.assertEqual('test-project-id', project_id) def test_info_value_not_present(self): From b57cf8d0f4825a75f23fa8c0d8b83629b6e3df32 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Fri, 27 May 2016 14:39:19 -0400 Subject: [PATCH 08/13] Use constant for project key. --- gcloud/_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index b04d9abf736a..a9e196be5d85 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -50,6 +50,7 @@ (?P\d{1,9}) # nanoseconds, maybe truncated Z # Zulu """, re.VERBOSE) +_PROJECT_KEY = 'project = ' class _LocalStack(Local): @@ -191,8 +192,8 @@ def _default_service_project_id(): gcloud_project_conf = gcloud_project_conf.split('\n') for key in gcloud_project_conf: - if key.startswith('project = '): - return key[10:] + if key.startswith(_PROJECT_KEY): + return key[len(_PROJECT_KEY):] return None From 72d72885fd4f3e35c3beb3d5cd33a09c5d1a25d9 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Tue, 31 May 2016 11:59:59 -0400 Subject: [PATCH 09/13] Get project ID from default configuration file --- gcloud/_helpers.py | 22 ++++++++++++---------- gcloud/test__helpers.py | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index a9e196be5d85..0e4dec105697 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -17,12 +17,15 @@ """ import calendar +try: + import configparser +except ImportError: + import ConfigParser as configparser import datetime import json import os import re import socket -import subprocess import sys from threading import local as Local @@ -185,16 +188,15 @@ def _default_service_project_id(): :rtype: str or ``NoneType`` :returns: Project-ID from ``gcloud info`` else ``None`` """ - command = subprocess.Popen(['gcloud', 'config', 'list', 'project'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True) - gcloud_project_conf = command.communicate()[0].decode('utf-8') - gcloud_project_conf = gcloud_project_conf.split('\n') + home_path = os.path.expanduser('~') + default_config_path = '.config/gcloud/configurations/config_default' + full_config_path = os.path.join(home_path, default_config_path) - for key in gcloud_project_conf: - if key.startswith(_PROJECT_KEY): - return key[len(_PROJECT_KEY):] + config = configparser.RawConfigParser() + config.read(full_config_path) + if config.has_section('core'): + return config.get('core', 'project') return None @@ -244,7 +246,7 @@ def _determine_default_project(project=None): * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file - * Get from `gcloud auth login` defaults + * Get default service project * Google App Engine application ID * Google Compute Engine project ID (from metadata server) diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index d0724c64befa..a8e7b2b9d30b 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -179,25 +179,49 @@ def test_no_environment(self): class Test__get_default_service_project_id(unittest2.TestCase): + temp_config_path = None + config_path = '.config/gcloud/configurations/' + config_file = 'config_default' - def callFUT(self, project_result=''): + def setUp(self): + import tempfile + import os + self.temp_config_path = tempfile.gettempdir() + + conf_path = os.path.join(self.temp_config_path, self.config_path) + os.makedirs(conf_path) + full_config_path = os.path.join(conf_path, self.config_file) + + with open(full_config_path, 'w') as conf_file: + conf_file.write('[core]\nproject = test-project-id') + + def tearDown(self): + import os + import shutil + + if self.temp_config_path: + shutil.rmtree(os.path.join(self.temp_config_path, + '.config')) + + def callFUT(self, project_id=None): from gcloud._helpers import _default_service_project_id - import subprocess + import os - def popen_communicate_mock(popen_object): - popen_object.kill() - return (project_result,) + def mock_expanduser(path=''): + if project_id and path == '~': + return self.temp_config_path + return '' from gcloud._testing import _Monkey - with _Monkey(subprocess.Popen, communicate=popen_communicate_mock): + with _Monkey(os.path, expanduser=mock_expanduser): return _default_service_project_id() def test_read_from_cli_info(self): - project_id = self.callFUT(b'project = test-project-id') + project_id = self.callFUT('test-project-id') self.assertEqual('test-project-id', project_id) def test_info_value_not_present(self): - project_id = self.callFUT(b'Active Configuration') + project_id = self.callFUT() self.assertEqual(None, project_id) From c75e9b2372679ad68505de6f74e02b195b7cbfc0 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Tue, 7 Jun 2016 15:11:32 -0400 Subject: [PATCH 10/13] Feedback updates --- docs/gcloud-config.rst | 47 +++++++++++++++++------------------------ gcloud/_helpers.py | 27 ++++++++--------------- gcloud/test__helpers.py | 6 ++++-- 3 files changed, 32 insertions(+), 48 deletions(-) diff --git a/docs/gcloud-config.rst b/docs/gcloud-config.rst index 43b06e882ffa..2c797060964d 100644 --- a/docs/gcloud-config.rst +++ b/docs/gcloud-config.rst @@ -4,26 +4,36 @@ Configuration Overview ======== -- Use service :class:`Client ` objects to configure +- Use service client objects to configure your applications. For example: .. code-block:: python - >>> from gcloud import bigquery - >>> client = bigquery.Client() + >>> from gcloud import bigquery + >>> client = bigquery.Client() -- :class:`Client ` objects hold both a ``project`` + You can override the detection of your default project by setting the + ``project`` parameter when creating client objects. + +.. code-block:: python + + >>> from gcloud import bigquery + >>> client = bigquery.Client(project='my-project') + +- Client objects hold both a ``project`` and an authenticated connection to a service. + .. code-block:: python + + >>> client.project + u'my-project' + - The authentication credentials can be implicitly determined from the - environment or directly via - :meth:`from_service_account_json ` - and - :meth:`from_service_account_p12 `. + environment or directly. See :doc:`./gcloud-auth`. -- Logging in with the `Google Cloud SDK`_ will automatically configure a JSON +- Logging in via ``gcloud auth login`` will automatically configure a JSON key file with your default project ID and credentials. Setting the ``GOOGLE_APPLICATION_CREDENTIALS`` and ``GCLOUD_PROJECT`` environment variables will override the automatically configured credentials. @@ -34,22 +44,3 @@ Overview .. code-block:: bash $ gcloud config set project my-new-default-project - -.. _Google Cloud SDK: http://cloud.google.com/sdk - -- You can override the credentials inferred from the environment by passing - explicit ``credentials`` to one of the alternative ``classmethod`` factories, - :meth:`gcloud.client.Client.from_service_account_json`: - - .. code-block:: python - - >>> from gcloud import bigquery - >>> client = bigquery.Client.from_service_account_json('/path/to/creds.json') - - or :meth:`gcloud.client.Client.from_service_account_p12`: - - .. code-block:: python - - >>> from gcloud import bigquery - >>> client = bigquery.Client.from_service_account_p12( - ... '/path/to/creds.p12', 'jrandom@example.com') diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 0e4dec105697..5d1393bedee2 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -17,10 +17,6 @@ """ import calendar -try: - import configparser -except ImportError: - import ConfigParser as configparser import datetime import json import os @@ -32,8 +28,10 @@ from google.protobuf import timestamp_pb2 import six from six.moves.http_client import HTTPConnection +from six.moves import configparser from gcloud.environment_vars import PROJECT +from gcloud.environment_vars import CREDENTIALS try: from google.appengine.api import app_identity @@ -53,7 +51,7 @@ (?P\d{1,9}) # nanoseconds, maybe truncated Z # Zulu """, re.VERBOSE) -_PROJECT_KEY = 'project = ' +DEFAULT_CONFIGURATION_PATH = '~/.config/gcloud/configurations/config_default' class _LocalStack(Local): @@ -172,32 +170,25 @@ def _file_project_id(): :returns: Project-ID from JSON credentials file if value exists, else ``None``. """ - credentials_file_path = os.getenv('GOOGLE_APPLICATION_CREDENTIALS') + credentials_file_path = os.getenv(CREDENTIALS) if credentials_file_path: - with open(credentials_file_path, 'r') as credentials_file: - credentials_data = credentials_file.read() - credentials = json.loads(credentials_data) + with open(credentials_file_path, 'rb') as credentials_file: + credentials = json.load(credentials_file) return credentials.get('project_id') - else: - return None def _default_service_project_id(): """Retrieves the project ID from the gcloud command line tool. :rtype: str or ``NoneType`` - :returns: Project-ID from ``gcloud info`` else ``None`` + :returns: Project-ID from default configuration file else ``None`` """ - home_path = os.path.expanduser('~') - default_config_path = '.config/gcloud/configurations/config_default' - full_config_path = os.path.join(home_path, default_config_path) - + full_config_path = os.path.expanduser(DEFAULT_CONFIGURATION_PATH) config = configparser.RawConfigParser() config.read(full_config_path) if config.has_section('core'): return config.get('core', 'project') - return None def _compute_engine_id(): @@ -246,7 +237,7 @@ def _determine_default_project(project=None): * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file - * Get default service project + * Get default service project from gcloud CLI tool * Google App Engine application ID * Google Compute Engine project ID (from metadata server) diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py index a8e7b2b9d30b..21bb8b002633 100644 --- a/gcloud/test__helpers.py +++ b/gcloud/test__helpers.py @@ -192,6 +192,8 @@ def setUp(self): os.makedirs(conf_path) full_config_path = os.path.join(conf_path, self.config_file) + self.temp_config_file = full_config_path + with open(full_config_path, 'w') as conf_file: conf_file.write('[core]\nproject = test-project-id') @@ -208,8 +210,8 @@ def callFUT(self, project_id=None): import os def mock_expanduser(path=''): - if project_id and path == '~': - return self.temp_config_path + if project_id and path.startswith('~'): + return self.temp_config_file return '' from gcloud._testing import _Monkey From cf419e6fd4aa808c5802d2f8996042e708eac925 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Wed, 8 Jun 2016 15:18:45 -0400 Subject: [PATCH 11/13] Update docs and json file handling --- docs/gcloud-config.rst | 62 +++++++++++++++++++++++++----------------- gcloud/_helpers.py | 9 ++++-- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/docs/gcloud-config.rst b/docs/gcloud-config.rst index 2c797060964d..21b3ffba8889 100644 --- a/docs/gcloud-config.rst +++ b/docs/gcloud-config.rst @@ -4,43 +4,55 @@ Configuration Overview ======== -- Use service client objects to configure - your applications. +Use service client objects to configure your applications. - For example: +For example: - .. code-block:: python +.. code-block:: python + + >>> from gcloud import bigquery + >>> client = bigquery.Client() + +When creating a client in this way, the project ID will be determined by +searching these locations in the following order. - >>> from gcloud import bigquery - >>> client = bigquery.Client() +* GCLOUD_PROJECT environment variable +* GOOGLE_APPLICATION_CREDENTIALS JSON file +* Default service configuration path from ``$ gcloud auth login``. +* Google App Engine application ID +* Google Compute Engine project ID (from metadata server) - You can override the detection of your default project by setting the - ``project`` parameter when creating client objects. +You can override the detection of your default project by setting the + ``project`` parameter when creating client objects. .. code-block:: python - >>> from gcloud import bigquery - >>> client = bigquery.Client(project='my-project') + >>> from gcloud import bigquery + >>> client = bigquery.Client(project='my-project') + +You can see what project ID a client is referencing by accessing the ``project`` +property on the client object. + +.. code-block:: python -- Client objects hold both a ``project`` - and an authenticated connection to a service. + >>> client.project + u'my-project' - .. code-block:: python +Authentication +============== - >>> client.project - u'my-project' +The authentication credentials can be implicitly determined from the +environment or directly. See :doc:`gcloud-auth`. -- The authentication credentials can be implicitly determined from the - environment or directly. See :doc:`./gcloud-auth`. +Logging in via ``gcloud auth login`` will automatically configure a JSON +key file with your default project ID and credentials. -- Logging in via ``gcloud auth login`` will automatically configure a JSON - key file with your default project ID and credentials. - Setting the ``GOOGLE_APPLICATION_CREDENTIALS`` and ``GCLOUD_PROJECT`` - environment variables will override the automatically configured credentials. +Setting the ``GOOGLE_APPLICATION_CREDENTIALS`` and ``GCLOUD_PROJECT`` +environment variables will override the automatically configured credentials. -- You can change your default project ID to ``my-new-default-project`` with - ``gcloud`` command line tool. +You can change your default project ID to ``my-new-default-project`` by +using the ``gcloud`` CLI tool to change the configuration. - .. code-block:: bash +.. code-block:: bash - $ gcloud config set project my-new-default-project + $ gcloud config set project my-new-default-project diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 5d1393bedee2..97d67362e399 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -173,13 +173,18 @@ def _file_project_id(): credentials_file_path = os.getenv(CREDENTIALS) if credentials_file_path: with open(credentials_file_path, 'rb') as credentials_file: - credentials = json.load(credentials_file) + credentials_json = credentials_file.read() + credentials = json.loads(credentials_json.decode('utf-8')) return credentials.get('project_id') def _default_service_project_id(): """Retrieves the project ID from the gcloud command line tool. + Files that cannot be opened with configparser are silently ignored; this is + designed so that you can specify a list of potential configuration file + locations. + :rtype: str or ``NoneType`` :returns: Project-ID from default configuration file else ``None`` """ @@ -237,7 +242,7 @@ def _determine_default_project(project=None): * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file - * Get default service project from gcloud CLI tool + * Get default service project from ``$ gcloud auth login`` * Google App Engine application ID * Google Compute Engine project ID (from metadata server) From d7b195147f713d35b1ecfa8d53b54d199e608ab2 Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Mon, 13 Jun 2016 09:31:11 -0400 Subject: [PATCH 12/13] Update with new authentication CLI command --- docs/gcloud-config.rst | 8 +++++--- gcloud/_helpers.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/gcloud-config.rst b/docs/gcloud-config.rst index 21b3ffba8889..8d30d7573cdb 100644 --- a/docs/gcloud-config.rst +++ b/docs/gcloud-config.rst @@ -18,7 +18,8 @@ searching these locations in the following order. * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file -* Default service configuration path from ``$ gcloud auth login``. +* Default service configuration path from + ``$ gcloud beta auth application-default login``. * Google App Engine application ID * Google Compute Engine project ID (from metadata server) @@ -44,8 +45,9 @@ Authentication The authentication credentials can be implicitly determined from the environment or directly. See :doc:`gcloud-auth`. -Logging in via ``gcloud auth login`` will automatically configure a JSON -key file with your default project ID and credentials. +Logging in via ``gcloud beta auth application-default login`` will +automatically configure a JSON key file with your default project ID and +credentials. Setting the ``GOOGLE_APPLICATION_CREDENTIALS`` and ``GCLOUD_PROJECT`` environment variables will override the automatically configured credentials. diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 97d67362e399..3f39d3608d99 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -242,7 +242,8 @@ def _determine_default_project(project=None): * GCLOUD_PROJECT environment variable * GOOGLE_APPLICATION_CREDENTIALS JSON file - * Get default service project from ``$ gcloud auth login`` + * Get default service project from + ``$ gcloud beta auth application-default login`` * Google App Engine application ID * Google Compute Engine project ID (from metadata server) From 3f408c118f374c30a7519d25283b13ba52bd376d Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Wed, 15 Jun 2016 15:01:28 -0400 Subject: [PATCH 13/13] Add win32 path for config --- gcloud/_helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py index 3f39d3608d99..05d1fd698791 100644 --- a/gcloud/_helpers.py +++ b/gcloud/_helpers.py @@ -189,8 +189,11 @@ def _default_service_project_id(): :returns: Project-ID from default configuration file else ``None`` """ full_config_path = os.path.expanduser(DEFAULT_CONFIGURATION_PATH) + win32_config_path = os.path.join(os.getenv('APPDATA', ''), + 'gcloud', 'configurations', + 'config_default') config = configparser.RawConfigParser() - config.read(full_config_path) + config.read([full_config_path, win32_config_path]) if config.has_section('core'): return config.get('core', 'project')