From 9101671c0ed17a5210ddeef298dba9bfb7516f7e Mon Sep 17 00:00:00 2001 From: durgesh-ninave-crest Date: Tue, 13 May 2025 18:36:36 +0530 Subject: [PATCH 1/3] feat(secretmanager): added support for regional secret manager --- plugins/lookup/gcp_secret_manager.py | 38 +++- plugins/modules/gcp_secret_manager.py | 175 ++++++++++++------ .../targets/gcp_secret_manager/aliases | 1 + .../gcp_secret_manager/defaults/main.yml | 3 + .../targets/gcp_secret_manager/meta/main.yml | 0 .../targets/gcp_secret_manager/tasks/main.yml | 9 + .../tasks/regionalsecrets.yml | 146 +++++++++++++++ .../tasks/regionalsecretslookup.yml | 76 ++++++++ .../gcp_secret_manager/tasks/secrets.yml | 137 ++++++++++++++ .../tasks/secretslookup.yml | 72 +++++++ 10 files changed, 595 insertions(+), 62 deletions(-) create mode 100644 tests/integration/targets/gcp_secret_manager/aliases create mode 100644 tests/integration/targets/gcp_secret_manager/defaults/main.yml create mode 100644 tests/integration/targets/gcp_secret_manager/meta/main.yml create mode 100644 tests/integration/targets/gcp_secret_manager/tasks/main.yml create mode 100644 tests/integration/targets/gcp_secret_manager/tasks/regionalsecrets.yml create mode 100644 tests/integration/targets/gcp_secret_manager/tasks/regionalsecretslookup.yml create mode 100644 tests/integration/targets/gcp_secret_manager/tasks/secrets.yml create mode 100644 tests/integration/targets/gcp_secret_manager/tasks/secretslookup.yml diff --git a/plugins/lookup/gcp_secret_manager.py b/plugins/lookup/gcp_secret_manager.py index 14824a92..19114faf 100644 --- a/plugins/lookup/gcp_secret_manager.py +++ b/plugins/lookup/gcp_secret_manager.py @@ -5,8 +5,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' + author: + - Dave Costakos name: gcp_secret_manager - author: Dave Costakos (@davecostakos) short_description: Get Secrets from Google Cloud as a Lookup plugin description: - retrieve secret keys in Secret Manager for use in playbooks @@ -14,6 +15,8 @@ DOCUMENTATION = ''' 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 to maintain secrecy of this value once returned. + - if location option is defined, then it deals with the regional secrets of the + location options: key: @@ -30,6 +33,10 @@ DOCUMENTATION = ''' - The name of the google cloud project - defaults to OS env variable GCP_PROJECT if not present type: str + location: + description: + - If provided, it defines the location of the regional secret. + type: str auth_kind: description: - the type of authentication to use with Google Cloud (i.e. serviceaccount or machineaccount) @@ -58,7 +65,7 @@ DOCUMENTATION = ''' 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: str + type: jsonarg required: False access_token: description: @@ -83,7 +90,6 @@ DOCUMENTATION = ''' description: - Authenticaiton scopes for Google Secret Manager type: list - elements: str default: ["https://www.googleapis.com/auth/cloud-platform"] ''' @@ -103,6 +109,22 @@ EXAMPLES = ''' - 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') }}" + +- name: Test regional secret using env variables for credentials + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1') }}" + +- name: Test regional secret using explicit credentials + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1', project='project', auth_kind='serviceaccount', service_account_file='file.json') }}" + +- name: Test getting specific version of a regional secret (old version) + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1', version='1') }}" + +- name: Test getting specific version of a regional secret (new version) + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1', version='2') }}" ''' RETURN = ''' @@ -168,6 +190,7 @@ class LookupModule(LookupBase): self.set_options(var_options=variables, direct=kwargs) params = { "key": self.get_option("key"), + "location": self.get_option("location"), "version": self.get_option("version"), "access_token": self.get_option("access_token"), "scopes": self.get_option("scopes"), @@ -199,7 +222,7 @@ class LookupModule(LookupBase): # to be set if secret versions get disabled # see https://issuetracker.google.com/issues/286489671 def get_latest_version(self, module, auth): - url = "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions?filter=state:ENABLED".format( + url = (self.make_url_prefix(module) + "secrets/{name}/versions?filter=state:ENABLED").format( **module.params ) response = auth.get(url) @@ -234,7 +257,7 @@ class LookupModule(LookupBase): if module.params['calc_version'] is None: return '' - url = "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions/{calc_version}:access".format( + url = (self.make_url_prefix(module) + "secrets/{name}/versions/{calc_version}:access").format( **module.params ) response = auth.get(url) @@ -244,3 +267,8 @@ class LookupModule(LookupBase): return '' return response.json()['payload']['data'] + + def make_url_prefix(self, module): + if module.params['location']: + return "https://secretmanager.{location}.rep.googleapis.com/v1/projects/{project}/locations/{location}/" + return "https://secretmanager.googleapis.com/v1/projects/{project}/" diff --git a/plugins/modules/gcp_secret_manager.py b/plugins/modules/gcp_secret_manager.py index a1e325e4..a6d150b0 100644 --- a/plugins/modules/gcp_secret_manager.py +++ b/plugins/modules/gcp_secret_manager.py @@ -24,8 +24,9 @@ description: - Create new secret values. - Add/remove versions of secrets. - Please note that other features like etags, replication, annontation expected to be managed outside of Ansible. +- Deals with regional secrets if location option is defined. short_description: Access and Update Google Cloud Secrets Manager objects -author: Dave Costakos (@davecostakos) +author: Dave Costakos @RedHat requirements: - python >= 2.6 - requests >= 2.18.4 @@ -44,7 +45,6 @@ options: - application - machineaccount - serviceaccount - - accesstoken service_account_contents: description: - The contents of a Service Account JSON file, either in a dictionary or as a @@ -59,21 +59,11 @@ options: - An optional service account email address if machineaccount is selected and the user does not wish to use the default email. type: str - access_token: - description: - - An OAuth2 access token if credential type is accesstoken. - type: str scopes: description: - Array of scopes to be used type: list elements: str - env_type: - description: - - Specifies which Ansible environment you're running this module within. - - This should not be set unless you know what you're doing. - - This only alters the User Agent string for any API requests. - type: str name: description: - Name of the secret to be used @@ -83,6 +73,10 @@ options: - key - secret - secret_id + location: + description: + - If provided, it defines the location of the regional secret. + type: str value: description: - The secret value that the secret should have @@ -116,7 +110,6 @@ options: - only used in creation - Note that the "value" piece of a label must contain only readable chars type: dict - default: {} notes: - 'API Reference: U(https://cloud.google.com/secret-manager/docs/reference/rests)' - 'Official Documentation: U(https://cloud.google.com/secret-manager/docs/overview)' @@ -176,6 +169,57 @@ EXAMPLES = r''' value: super_secret labels: key_name: "ansible_rox" + +- name: Create a new regional secret + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + value: super_secret + state: present + auth_kind: serviceaccount + service_account_file: service_account_creds.json + +- name: Ensure the regional secret exists, fail otherwise and return the value + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + state: present + +- name: Ensure regional secret exists but don't return the value + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + state: present + return_value: false + +- name: Add a new version of a regional secret + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + value: updated super secret + state: present + +- name: Delete version 1 of a regional secret (but not the secret itself) + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + version: 1 + state: absent + +- name: Delete all versions of a regional secret + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + version: all + state: absent + +- name: Create a regional secret with labels + google.cloud.gcp_secret_manager: + name: secret_key + location: us-central1 + value: super_secret + labels: + key_name: "ansible_rox" ''' RETURN = r''' @@ -183,42 +227,46 @@ resources: description: List of resources returned: always type: complex - contains: - name: - description: - - The name of the secret - returned: success - type: str - version: - description: - - the version number of the secret returned - returned: success - type: str - url: - description: - - the Google Cloud URL used to make the request - returned: success - type: str - status_code: - description: - - the HTTP status code of the response to Google Cloud - returned: success - type: str - msg: - description: - - A message indicating what was done (or not done) - returned: success, failure - type: str - value: - description: - - The decrypted secret value, please use care with this - returned: success - type: str - payload: - description: - - The base 64 secret payload including CRC for validation - returned: success - type: dict + name: + description: + - The name of the secret + returned: success + type: str + location: + description: + - The location of the regional secret. + returned: success + type: str + version: + description: + - the version number of the secret returned + returned: success + type: str + url: + description: + - the Google Cloud URL used to make the request + returned: success + type: str + status_code: + description: + - the HTTP status code of the response to Google Cloud + returned: success + type: str + msg: + description: + - A message indicating what was done (or not done) + returned: success, failure + type: str + value: + description: + - The decrypted secret value, please use care with this + returned: success + type: str + payload: + description: + - The base 64 secret payload including CRC for validation + retunred: success + type: dict ''' ################################################################################ @@ -241,24 +289,30 @@ def get_auth(module): return GcpSession(module, 'secret-manager') +def make_url_prefix(module): + if module.params['location']: + return "https://secretmanager.{location}.rep.googleapis.com/v1/projects/{project}/locations/{location}/" + return "https://secretmanager.googleapis.com/v1/projects/{project}/" + + def self_access_link(module): - return "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions/{calc_version}:access".format(**module.params) + return (make_url_prefix(module) + "secrets/{name}/versions/{calc_version}:access").format(**module.params) def self_get_link(module): - return "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions/{calc_version}".format(**module.params) + return (make_url_prefix(module) + "secrets/{name}/versions/{calc_version}").format(**module.params) def self_update_link(module): - return "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions/{calc_version:version}".format(**module.params) + return (make_url_prefix(module) + "secrets/{name}/versions/{calc_version:version}").format(**module.params) def self_list_link(module): - return "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}/versions?filter=state:ENABLED".format(**module.params) + return (make_url_prefix(module) + "secrets/{name}/versions?filter=state:ENABLED").format(**module.params) def self_delete_link(module): - return "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}".format(**module.params) + return (make_url_prefix(module) + "secrets/{name}").format(**module.params) def fetch_resource(module, allow_not_found=True): @@ -307,10 +361,12 @@ def merge_dicts(x, y): def create_secret(module): # build the payload payload = {"replication": {"automatic": {}}} + if module.params['location']: + payload = dict() if module.params['labels']: payload['labels'] = module.params['labels'] - url = "https://secretmanager.googleapis.com/v1/projects/{project}/secrets".format(**module.params) + url = (make_url_prefix(module) + "secrets").format(**module.params) auth = get_auth(module) post_response = auth.post(url, body=payload, params={'secretId': module.params['name']}) # validate create @@ -327,7 +383,7 @@ def update_secret(module): } } auth = get_auth(module) - url = "https://secretmanager.googleapis.com/v1/projects/{project}/secrets/{name}:addVersion".format(**module.params) + url = (make_url_prefix(module) + "secrets/{name}:addVersion").format(**module.params) return return_if_object(module, auth.post(url, payload), False) @@ -376,7 +432,11 @@ def return_if_object(module, response, allow_not_found=False): result['status_code'] = response.status_code if "name" in result: result['version'] = result['name'].split("/")[-1] - result['name'] = result['name'].split("/")[3] + if 'locations' in result['name'].split("/"): + result['location'] = result['name'].split("/")[3] + result['name'] = result['name'].split("/")[5] + else: + result['name'] = result['name'].split("/")[3] # base64 decode the value if "payload" in result and "data" in result['payload']: @@ -401,6 +461,7 @@ def main(): argument_spec=dict( state=dict(default='present', choices=['present', 'absent'], type='str'), name=dict(required=True, type='str', aliases=['key', 'secret', 'secret_id']), + location=dict(required=False, type='str'), value=dict(required=False, type='str'), version=dict(required=False, type='str', default='latest'), return_value=dict(required=False, type='bool', default=True), diff --git a/tests/integration/targets/gcp_secret_manager/aliases b/tests/integration/targets/gcp_secret_manager/aliases new file mode 100644 index 00000000..0e4419e3 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/aliases @@ -0,0 +1 @@ +cloud/gcp \ No newline at end of file diff --git a/tests/integration/targets/gcp_secret_manager/defaults/main.yml b/tests/integration/targets/gcp_secret_manager/defaults/main.yml new file mode 100644 index 00000000..61fa8b04 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/defaults/main.yml @@ -0,0 +1,3 @@ +--- +resource_name: "{{ resource_prefix }}" +lookup_resource_name: "{{ resource_prefix }}_lookup" diff --git a/tests/integration/targets/gcp_secret_manager/meta/main.yml b/tests/integration/targets/gcp_secret_manager/meta/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/targets/gcp_secret_manager/tasks/main.yml b/tests/integration/targets/gcp_secret_manager/tasks/main.yml new file mode 100644 index 00000000..37aace00 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: Secrets tests + ansible.builtin.include_tasks: secrets.yml +- name: Secrets lookup tests + ansible.builtin.include_tasks: secretslookup.yml +- name: Regional Secrets tests + ansible.builtin.include_tasks: regionalsecrets.yml +- name: Regional Secrets lookup tests + ansible.builtin.include_tasks: regionalsecretslookup.yml diff --git a/tests/integration/targets/gcp_secret_manager/tasks/regionalsecrets.yml b/tests/integration/targets/gcp_secret_manager/tasks/regionalsecrets.yml new file mode 100644 index 00000000..02abbe35 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/tasks/regionalsecrets.yml @@ -0,0 +1,146 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# Pre-test setup +- name: Delete the regional test secret if it exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + version: "all" + location: "us-central1" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent +# ---------------------------------------------------------- +- name: Create a regional secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + value: "ansible-test-regional-secret-value" + labels: + key1: "val1" + key2: "val2" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Create a regional secret that already exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + value: "ansible-test-regional-secret-value" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Add a new version to a regional secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + value: "ansible-test-regional-secret-value-updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Add a version that exists to a regional secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + value: "ansible-test-regional-secret-value-updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Ensure the regional secret exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Delete the regional secret version + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + version: "1" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Delete the regional secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Delete the regional secret that does not exist + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + location: "us-central1" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false diff --git a/tests/integration/targets/gcp_secret_manager/tasks/regionalsecretslookup.yml b/tests/integration/targets/gcp_secret_manager/tasks/regionalsecretslookup.yml new file mode 100644 index 00000000..1a9c3a52 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/tasks/regionalsecretslookup.yml @@ -0,0 +1,76 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# Pre-test setup +- name: Delete the regional test secret if it exists + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + version: "all" + location: "us-central1" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent +- name: Create a regional secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + location: "us-central1" + value: "ansible lookup test regional secret value" + labels: + key1: "val1" + key2: "val2" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present +- name: Add a new version to a regional secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + location: "us-central1" + value: "ansible lookup test regional secret value updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present +# ---------------------------------------------------------- +- name: Retrieve the latest secret version of a regional secret + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key=lookup_resource_name, location='us-central1', project=gcp_project, auth_kind=gcp_cred_kind, service_account_file=gcp_cred_file | default(omit)) }}" + register: result +- name: Assert secret value + ansible.builtin.assert: + that: + - result.msg == "ansible lookup test regional secret value updated" +# ---------------------------------------------------------- +- name: Retrieve the specified secret version of a regional secret + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key=lookup_resource_name, location='us-central1', version='1', project=gcp_project, auth_kind=gcp_cred_kind, service_account_file=gcp_cred_file | default(omit)) }}" + register: result +- name: Assert secret value + ansible.builtin.assert: + that: + - result.msg == "ansible lookup test regional secret value" +# --------------------------------------------------------- +# Post-test teardown +# If errors happen, don't crash the playbook! +- name: Delete the regional test secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + location: "us-central1" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + ignore_errors: true diff --git a/tests/integration/targets/gcp_secret_manager/tasks/secrets.yml b/tests/integration/targets/gcp_secret_manager/tasks/secrets.yml new file mode 100644 index 00000000..16d656db --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/tasks/secrets.yml @@ -0,0 +1,137 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# Pre-test setup +- name: Delete the test secret if it exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent +# ---------------------------------------------------------- +- name: Create a secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + value: "ansible-test-secret-value" + labels: + key1: "val1" + key2: "val2" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Create a secret that already exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + value: "ansible-test-secret-value" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Add a new version to a secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + value: "ansible-test-secret-value-updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Add a version that exists to a secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + value: "ansible-test-secret-value-updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Ensure the secret exists + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false +# ---------------------------------------------------------- +- name: Delete the secret version + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + version: "1" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Delete the secret + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is true + ansible.builtin.assert: + that: + - result.changed == true +# ---------------------------------------------------------- +- name: Delete the secret that does not exist + google.cloud.gcp_secret_manager: + name: "{{ resource_name }}" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + register: result +- name: Assert changed is false + ansible.builtin.assert: + that: + - result.changed == false diff --git a/tests/integration/targets/gcp_secret_manager/tasks/secretslookup.yml b/tests/integration/targets/gcp_secret_manager/tasks/secretslookup.yml new file mode 100644 index 00000000..2ba9c2f3 --- /dev/null +++ b/tests/integration/targets/gcp_secret_manager/tasks/secretslookup.yml @@ -0,0 +1,72 @@ +# Copyright 2024 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# Pre-test setup +- name: Delete the test secret if it exists + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent +- name: Create a secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + value: "ansible lookup test secret value" + labels: + key1: "val1" + key2: "val2" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present +- name: Add a new version to a secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + value: "ansible lookup test secret value updated" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present +# ---------------------------------------------------------- +- name: Retrieve the latest secret version of a secret + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key=lookup_resource_name, project=gcp_project, auth_kind=gcp_cred_kind, service_account_file=gcp_cred_file | default(omit)) }}" + register: result +- name: Assert secret value + ansible.builtin.assert: + that: + - result.msg == "ansible lookup test secret value updated" +# ---------------------------------------------------------- +- name: Retrieve the specified secret version of a secret + ansible.builtin.debug: + msg: "{{ lookup('google.cloud.gcp_secret_manager', key=lookup_resource_name, version='1', project=gcp_project, auth_kind=gcp_cred_kind, service_account_file=gcp_cred_file | default(omit)) }}" + register: result +- name: Assert secret value + ansible.builtin.assert: + that: + - result.msg == "ansible lookup test secret value" +# --------------------------------------------------------- +# Post-test teardown +# If errors happen, don't crash the playbook! +- name: Delete the test secret + google.cloud.gcp_secret_manager: + name: "{{ lookup_resource_name }}" + version: "all" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + ignore_errors: true From 695fd4b35544aa8877100d80721fe57d58ffa5f3 Mon Sep 17 00:00:00 2001 From: durgesh-ninave-crest Date: Mon, 19 May 2025 18:15:57 +0530 Subject: [PATCH 2/3] update documentation of module and lookup --- plugins/lookup/gcp_secret_manager.py | 3 ++- plugins/modules/gcp_secret_manager.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/lookup/gcp_secret_manager.py b/plugins/lookup/gcp_secret_manager.py index 19114faf..754f4af0 100644 --- a/plugins/lookup/gcp_secret_manager.py +++ b/plugins/lookup/gcp_secret_manager.py @@ -65,7 +65,7 @@ DOCUMENTATION = ''' 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 + type: str required: False access_token: description: @@ -90,6 +90,7 @@ DOCUMENTATION = ''' description: - Authenticaiton scopes for Google Secret Manager type: list + elements: str default: ["https://www.googleapis.com/auth/cloud-platform"] ''' diff --git a/plugins/modules/gcp_secret_manager.py b/plugins/modules/gcp_secret_manager.py index a6d150b0..6883d703 100644 --- a/plugins/modules/gcp_secret_manager.py +++ b/plugins/modules/gcp_secret_manager.py @@ -45,6 +45,7 @@ options: - application - machineaccount - serviceaccount + - accesstoken service_account_contents: description: - The contents of a Service Account JSON file, either in a dictionary or as a @@ -59,11 +60,21 @@ options: - An optional service account email address if machineaccount is selected and the user does not wish to use the default email. type: str + access_token: + description: + - An OAuth2 access token if credential type is accesstoken. + type: str scopes: description: - Array of scopes to be used type: list elements: str + env_type: + description: + - Specifies which Ansible environment you're running this module within. + - This should not be set unless you know what you're doing. + - This only alters the User Agent string for any API requests. + type: str name: description: - Name of the secret to be used From ac08c81321a8957d4f31d846269123ab40233278 Mon Sep 17 00:00:00 2001 From: durgesh-ninave-crest Date: Thu, 5 Jun 2025 12:00:49 +0530 Subject: [PATCH 3/3] fix sanity test issues --- plugins/lookup/gcp_secret_manager.py | 6 +- plugins/modules/gcp_secret_manager.py | 89 ++++++++++++++------------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/plugins/lookup/gcp_secret_manager.py b/plugins/lookup/gcp_secret_manager.py index 754f4af0..f8125d05 100644 --- a/plugins/lookup/gcp_secret_manager.py +++ b/plugins/lookup/gcp_secret_manager.py @@ -5,8 +5,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' - author: - - Dave Costakos + author: Google Inc. (@googlecloudplatform) name: gcp_secret_manager short_description: Get Secrets from Google Cloud as a Lookup plugin description: @@ -117,7 +116,8 @@ EXAMPLES = ''' - name: Test regional secret using explicit credentials ansible.builtin.debug: - msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1', project='project', auth_kind='serviceaccount', service_account_file='file.json') }}" + msg: "{{ lookup('google.cloud.gcp_secret_manager', key='secret_key', location='us-central1', project='project', auth_kind='serviceaccount', + service_account_file='file.json') }}" - name: Test getting specific version of a regional secret (old version) ansible.builtin.debug: diff --git a/plugins/modules/gcp_secret_manager.py b/plugins/modules/gcp_secret_manager.py index 6883d703..bff1d07d 100644 --- a/plugins/modules/gcp_secret_manager.py +++ b/plugins/modules/gcp_secret_manager.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt # or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -26,7 +27,7 @@ description: - Please note that other features like etags, replication, annontation expected to be managed outside of Ansible. - Deals with regional secrets if location option is defined. short_description: Access and Update Google Cloud Secrets Manager objects -author: Dave Costakos @RedHat +author: Google Inc. (@googlecloudplatform) requirements: - python >= 2.6 - requests >= 2.18.4 @@ -121,6 +122,7 @@ options: - only used in creation - Note that the "value" piece of a label must contain only readable chars type: dict + default: {} notes: - 'API Reference: U(https://cloud.google.com/secret-manager/docs/reference/rests)' - 'Official Documentation: U(https://cloud.google.com/secret-manager/docs/overview)' @@ -136,7 +138,7 @@ notes: - The I(service_account_email) and I(service_account_file) options are mutually exclusive. ''' -EXAMPLES = r''' +EXAMPLES = ''' - name: Create a new secret google.cloud.gcp_secret_manager: name: secret_key @@ -233,51 +235,52 @@ EXAMPLES = r''' key_name: "ansible_rox" ''' -RETURN = r''' +RETURN = ''' resources: description: List of resources returned: always type: complex - name: - description: - - The name of the secret - returned: success - type: str - location: - description: - - The location of the regional secret. - returned: success - type: str - version: - description: - - the version number of the secret returned - returned: success - type: str - url: - description: - - the Google Cloud URL used to make the request - returned: success - type: str - status_code: - description: - - the HTTP status code of the response to Google Cloud - returned: success - type: str - msg: - description: - - A message indicating what was done (or not done) - returned: success, failure - type: str - value: - description: - - The decrypted secret value, please use care with this - returned: success - type: str - payload: - description: - - The base 64 secret payload including CRC for validation - retunred: success - type: dict + contains: + name: + description: + - The name of the secret + returned: success + type: str + location: + description: + - The location of the regional secret. + returned: success + type: str + version: + description: + - The version number of the secret returned + returned: success + type: str + url: + description: + - The Google Cloud URL used to make the request + returned: success + type: str + status_code: + description: + - The HTTP status code of the response to Google Cloud + returned: success + type: str + msg: + description: + - A message indicating what was done (or not done) + returned: success, failure + type: str + value: + description: + - The decrypted secret value, please use care with this + returned: success + type: str + payload: + description: + - The base 64 secret payload including CRC for validation + returned: success + type: dict ''' ################################################################################