-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding support for Google Secret Manager for issue 543 #578
Merged
SirGitsalot
merged 5 commits into
ansible-collections:master
from
dcostakos:gcp_secret_manager
May 22, 2024
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
76eb024
Adding support for Google Secret Manager for issue 543
dcostakos 953b06f
updated lookuup plugin based on comment https://github.com/ansible-co…
dcostakos 375b317
Merge branch 'ansible-collections:master' into gcp_secret_manager
dcostakos 3ce29db
updated plugsins based on feedback, fixed linting and documentation e…
dcostakos 40d2c9a
updated plugsins based on feedback, fixed linting and documentation e…
dcostakos File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,213 @@ | ||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||||||
# SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|
||||||
from __future__ import (absolute_import, division, print_function) | ||||||
__metaclass__ = type | ||||||
|
||||||
DOCUMENTATION = ''' | ||||||
author: | ||||||
- Dave Costakos <dcostako@redhat.com> | ||||||
name: gcp_secret_manager | ||||||
short_description: Get Secrets from Google Cloud as a Lookup plugin | ||||||
description: | ||||||
- retrieve secret keys in Secret Manager for use in playbooks | ||||||
- see https://cloud.google.com/iam/docs/service-account-creds for details on creating | ||||||
credentials for Google Cloud and the format of such credentials | ||||||
- once a secret value is retreived, it is returned decoded. It is up to the developer | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
to maintain secrecy of this value once returned. | ||||||
|
||||||
options: | ||||||
key: | ||||||
description: | ||||||
- the key of the secret to look up in Secret Manager | ||||||
type: str | ||||||
required: True | ||||||
project: | ||||||
description: | ||||||
- The name of the google cloud project | ||||||
- defaults to OS env variable GCP_PROJECT if not present | ||||||
type: str | ||||||
auth_kind: | ||||||
description: | ||||||
- the type of authentication to use with Google Cloud (i.e. serviceaccount or machineaccount) | ||||||
- defaults to OS env variable GCP_AUTH_KIND if not present | ||||||
type: str | ||||||
version: | ||||||
description: | ||||||
- the version name of your secret to retrieve | ||||||
type: str | ||||||
default: latest | ||||||
required: False | ||||||
service_account_email: | ||||||
description: | ||||||
- email associated with the service account | ||||||
- defaults to OS env variable GCP_SERVICE_ACCOUNT_EMAIL if not present | ||||||
type: str | ||||||
required: False | ||||||
service_account_file: | ||||||
description: | ||||||
- JSON Credential file obtained from Google Cloud | ||||||
- defaults to OS env variable GCP_SERVICE_ACCOUNT_FILE if not present | ||||||
- see https://cloud.google.com/iam/docs/service-account-creds for details | ||||||
type: str | ||||||
required: False | ||||||
service_account_info: | ||||||
description: | ||||||
- JSON Object representing the contents of a service_account_file obtained from Google Cloud | ||||||
- defaults to OS env variable GCP_SERVICE_ACCOUNT_INFO if not present | ||||||
type: jsonarg | ||||||
required: False | ||||||
errors: | ||||||
description: | ||||||
- how to handle errors | ||||||
choices: ['strict','warn','ignore'] | ||||||
default: strict | ||||||
''' | ||||||
|
||||||
EXAMPLES = ''' | ||||||
- name: Test secret using env variables for credentials | ||||||
ansible.builtin.debug: | ||||||
msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key') }}" | ||||||
|
||||||
- name: Test secret using explicit credentials | ||||||
ansible.builtin.debug: | ||||||
msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', project='project', auth_kind='serviceaccount', service_account_file='file.json') }}" | ||||||
|
||||||
- name: Test getting specific version of a secret (old version) | ||||||
ansible.builtin.debug: | ||||||
msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', version='1') }}" | ||||||
|
||||||
- name: Test getting specific version of a secret (new version) | ||||||
ansible.builtin.debug: | ||||||
msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', version='2') }}" | ||||||
''' | ||||||
|
||||||
RETURN = ''' | ||||||
_raw: | ||||||
description: the contents of the secret requested (please use "no_log" to not expose this secret) | ||||||
type: list | ||||||
elements: str | ||||||
''' | ||||||
|
||||||
################################################################################ | ||||||
# Imports | ||||||
################################################################################ | ||||||
|
||||||
import json | ||||||
import os | ||||||
import base64 | ||||||
|
||||||
|
||||||
from ansible.plugins.lookup import LookupBase | ||||||
|
||||||
try: | ||||||
import requests | ||||||
HAS_REQUESTS = True | ||||||
except ImportError: | ||||||
HAS_REQUESTS = False | ||||||
|
||||||
try: | ||||||
import google.auth | ||||||
from google.oauth2 import service_account | ||||||
from google.auth.transport.requests import AuthorizedSession | ||||||
HAS_GOOGLE_LIBRARIES = True | ||||||
except ImportError: | ||||||
HAS_GOOGLE_LIBRARIES = False | ||||||
|
||||||
from ansible_collections.google.cloud.plugins.module_utils.gcp_utils import GcpSession, GcpRequest | ||||||
from ansible.errors import AnsibleError | ||||||
|
||||||
class GcpLookupException(Exception): | ||||||
pass | ||||||
|
||||||
class LookupModule(LookupBase): | ||||||
def run(self, terms, variables, **kwargs): | ||||||
self.set_options(var_options=variables, direct=kwargs) | ||||||
self.scopes = ["https://www.googleapis.com/auth/cloud-platform"] | ||||||
self._validate() | ||||||
self.service_acct_creds = self._credentials() | ||||||
session = AuthorizedSession(self.service_acct_creds) | ||||||
response = session.get("https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{key}/versions/{version}:access".format(**self.get_options())) | ||||||
if response.status_code == 200: | ||||||
result_data = response.json() | ||||||
secret_value = base64.b64decode(result_data['payload']['data']) | ||||||
return [ secret_value ] | ||||||
else: | ||||||
if self.get_option('errors') == 'warn': | ||||||
self.warn(f"secret request returned bad status: {response.status_code} {response.json()}") | ||||||
return [ '' ] | ||||||
elif self.get_option('error') == 'ignore': | ||||||
return [ '' ] | ||||||
else: | ||||||
raise AnsibleError(f"secret request returned bad status: {response.status_code} {response.json()}") | ||||||
|
||||||
def _validate(self): | ||||||
if HAS_GOOGLE_LIBRARIES == False: | ||||||
raise AnsibleError("Please install the google-auth library") | ||||||
|
||||||
if HAS_REQUESTS == False: | ||||||
raise AnsibleError("Please install the requests library") | ||||||
|
||||||
if self.get_option('key') == None: | ||||||
raise AnsibleError("'key' is a required parameter") | ||||||
|
||||||
if self.get_option('version') == None: | ||||||
self.set_option('version', 'latest') | ||||||
|
||||||
self._set_from_env('project', 'GCP_PROJECT', True) | ||||||
self._set_from_env('auth_kind', 'GCP_AUTH_KIND', True) | ||||||
self._set_from_env('service_account_email', 'GCP_SERVICE_ACCOUNT_EMAIL') | ||||||
self._set_from_env('service_account_file', 'GCP_SERVICE_ACCOUNT_FILE') | ||||||
self._set_from_env('service_account_info', 'GCP_SERVICE_ACCOUNT_INFO') | ||||||
|
||||||
def _set_from_env(self, var=None, env_name=None, raise_on_empty=False): | ||||||
if self.get_option(var) == None: | ||||||
if env_name is not None and env_name in os.environ: | ||||||
fallback = os.environ[env_name] | ||||||
self.set_option(var, fallback) | ||||||
|
||||||
if self.get_option(var) == None and raise_on_empty: | ||||||
msg = f"No key '{var}' provided" | ||||||
if env_name is not None: | ||||||
msg += f" and no fallback to env['{env_name}'] available" | ||||||
raise AnsibleError(msg) | ||||||
|
||||||
def _credentials(self): | ||||||
cred_type = self.get_option('auth_kind') | ||||||
|
||||||
if cred_type == 'application': | ||||||
credentials, project_id = google.auth.default(scopes=self.scopes) | ||||||
return credentials | ||||||
|
||||||
if cred_type == 'serviceaccount': | ||||||
if self.get_option('service_account_file') is not None: | ||||||
path = os.path.realpath(os.path.expanduser(self.get_option('service_account_file'))) | ||||||
try: | ||||||
svc_acct_creds = service_account.Credentials.from_service_account_file(path) | ||||||
except OSError as e: | ||||||
raise GcpLookupException("Unable to read service_account_file at %s: %s" % (path, e.strerror)) | ||||||
|
||||||
elif self.get_option('service_account_contents') is not None: | ||||||
try: | ||||||
info = json.loads(self.get_option('service_account_contents')) | ||||||
except json.decoder.JSONDecodeError as e: | ||||||
raise GcpLookupException("Unable to decode service_account_contents as JSON: %s" % e) | ||||||
|
||||||
svc_acct_creds = service_account.Credentials.from_service_account_info(info) | ||||||
else: | ||||||
raise GcpLookupException('Service Account authentication requires setting either service_account_file or service_account_contents') | ||||||
|
||||||
return svc_acct_creds.with_scopes(self.scopes) | ||||||
|
||||||
if cred_type == 'machineaccount': | ||||||
self.svc_acct_creds = google.auth.compute_engine.Credentials(self.service_account_email) | ||||||
return self.svc_acct_creds | ||||||
|
||||||
raise GcpLookupException("Credential type '%s' not implemented" % cred_type) | ||||||
|
||||||
|
||||||
|
||||||
|
||||||
|
||||||
|
||||||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for implementing this - it's awesome to see this feature being worked on.
In terms of the code for this lookup, is there a reason of not leveraging
ansible_collections.google.cloud.plugins.module_utils.gcp_utils
as a helper for handling the authentication workflow? IMO it would simplify the code and also gain the benefit of being able to use the new OAUTH token as well - recently added by this PR - #574.I have used it in my private Collection and tested working fine. Code snippet here - you can add the additional env handling, but the heavy lifting of the authentication workflows and API requests are taken care of by
gcp_utils
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for this suggestion. I'll integrate these changes.