From f5f6cf467e2411c92a13f6ddd827a6cb405a69ea Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Thu, 16 Mar 2017 17:10:07 -0700 Subject: [PATCH] [GCE] Support default credentials (#22723) Add support for default credentials. Practically, this means that a playbook creator would not have to specify the service_account_email or credentials_file Ansible parameters. Default Credentials only work when running on Google Cloud Platform. The 'project_id' is still required. A test has been added to trigger this condition. --- lib/ansible/module_utils/gcp.py | 35 ++++++++++---- test/units/module_utils/gcp/test_auth.py | 61 ++++++++++++++++++------ 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/lib/ansible/module_utils/gcp.py b/lib/ansible/module_utils/gcp.py index 57076141f3..026efa582d 100644 --- a/lib/ansible/module_utils/gcp.py +++ b/lib/ansible/module_utils/gcp.py @@ -155,6 +155,8 @@ def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False): Additionally, flags may be set to require valid json and check the libcloud version. + AnsibleModule.fail_json is called only if the project_id cannot be found. + :param module: initialized Ansible module object :type module: `class AnsibleModule` @@ -190,19 +192,27 @@ def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False): if credentials_file is None or project_id is None or service_account_email is None: if check_libcloud is True: - # TODO(supertom): this message is legacy and integration tests depend on it. - module.fail_json(msg='Missing GCE connection parameters in libcloud ' - 'secrets file.') - return None + if project_id is None: + # TODO(supertom): this message is legacy and integration tests depend on it. + module.fail_json(msg='Missing GCE connection parameters in libcloud ' + 'secrets file.') else: - if credentials_file is None or project_id is None: + if project_id is None: module.fail_json(msg=('GCP connection error: unable to determine project (%s) or ' 'credentials file (%s)' % (project_id, credentials_file))) + # Set these fields to empty strings if they are None + # consumers of this will make the distinction between an empty string + # and None. + if credentials_file is None: + credentials_file = '' + if service_account_email is None: + service_account_email = '' # ensure the credentials file is found and is in the proper format. - _validate_credentials_file(module, credentials_file, - require_valid_json=require_valid_json, - check_libcloud=check_libcloud) + if credentials_file: + _validate_credentials_file(module, credentials_file, + require_valid_json=require_valid_json, + check_libcloud=check_libcloud) return {'service_account_email': service_account_email, 'credentials_file': credentials_file, @@ -279,6 +289,9 @@ def get_google_cloud_credentials(module, scopes=[]): """ Get credentials object for use with Google Cloud client. + Attempts to obtain credentials by calling _get_gcp_credentials. If those are + not present will attempt to connect via Application Default Credentials. + To connect via libcloud, don't use this function, use gcp_connect instead. For Google Python API Client, see get_google_api_auth for how to connect. @@ -314,8 +327,10 @@ def get_google_cloud_credentials(module, scopes=[]): if scopes: credentials = credentials.with_scopes(scopes) else: - credentials = google.auth.default( - scopes=scopes)[0] + (credentials, project_id) = google.auth.default( + scopes=scopes) + if project_id is not None: + conn_params['project_id'] = project_id return (credentials, conn_params) except Exception as e: diff --git a/test/units/module_utils/gcp/test_auth.py b/test/units/module_utils/gcp/test_auth.py index 7f58ea3fcd..0e3adcd903 100644 --- a/test/units/module_utils/gcp/test_auth.py +++ b/test/units/module_utils/gcp/test_auth.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# (c) 2016, Tom Melendez +# (c) 2016, Tom Melendez (@supertom) # # This file is part of Ansible # @@ -18,8 +18,10 @@ import os import sys +import pytest + from ansible.compat.tests import mock, unittest -from ansible.module_utils.gcp import (_get_gcp_ansible_credentials, _get_gcp_environ_var, +from ansible.module_utils.gcp import (_get_gcp_ansible_credentials, _get_gcp_credentials, _get_gcp_environ_var, _get_gcp_libcloud_credentials, _get_gcp_environment_credentials, _validate_credentials_file) @@ -31,21 +33,32 @@ def fake_get_gcp_environ_var(var_name, default_value): else: return fake_env_data[var_name] +# Fake AnsibleModule for use in tests +class FakeModule(object): + class Params(): + data = {} + + def get(self, key, alt=None): + if key in self.data: + return self.data[key] + else: + return alt + + def __init__(self, data={}): + self.params = FakeModule.Params() + self.params.data = data + + def fail_json(self, **kwargs): + raise ValueError("fail_json") + class GCPAuthTestCase(unittest.TestCase): """Tests to verify different Auth mechanisms.""" + + def setup_method(self, method): + global fake_env_data + fake_env_data = {'GCE_EMAIL': 'gce-email'} + def test_get_gcp_ansible_credentials(self): - # create a fake (AnsibleModule) object to pass to the function - class FakeModule(object): - class Params(): - data = {} - def get(self, key, alt=None): - if key in self.data: - return self.data[key] - else: - return alt - def __init__(self, data={}): - self.params = FakeModule.Params() - self.params.data = data input_data = {'service_account_email': 'mysa', 'credentials_file': 'path-to-file.json', 'project_id': 'my-cool-project'} @@ -179,3 +192,23 @@ class GCPAuthTestCase(unittest.TestCase): expected = tuple(['my-sa-email', '/path/to/creds.json', 'my-project']) actual = _get_gcp_environment_credentials('my-sa-email', '/path/to/creds.json', None) self.assertEqual(expected, actual) + + @mock.patch('ansible.module_utils.gcp._get_gcp_environ_var', + side_effect=fake_get_gcp_environ_var) + def test_get_gcp_credentials(self, mockobj): + global fake_env_data + + fake_env_data = {} + module = FakeModule() + module.params.data = {} + # Nothing is set, calls fail_json + with pytest.raises(ValueError): + _get_gcp_credentials(module) + + # project_id (only) is set from Ansible params. + module.params.data['project_id'] = 'my-project' + actual = _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False) + expected = {'service_account_email': '', + 'project_id': 'my-project', + 'credentials_file': ''} + self.assertEqual(expected, actual)