Skip to content

Commit 0bf2444

Browse files
authored
Merge pull request #684 from durgesh-ninave-crest/add-support-for-parameter-manager-module-lookup
feat(parametermanager): added support for parameter manager module and lookup
2 parents 8863545 + ccdc27f commit 0bf2444

10 files changed

Lines changed: 1908 additions & 0 deletions

File tree

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
from __future__ import (absolute_import, division, print_function)
5+
__metaclass__ = type
6+
7+
DOCUMENTATION = '''
8+
name: gcp_parameter_manager
9+
author: Google Inc. (@googlecloudplatform)
10+
11+
short_description: Get Parameters from Google Cloud as a Lookup plugin
12+
description:
13+
- retrieve parameter keys in parameter Manager for use in playbooks
14+
- see https://cloud.google.com/iam/docs/service-account-creds for details on creating
15+
credentials for Google Cloud and the format of such credentials
16+
- once a parameter value is retreived, it is returned decoded. It is up to the developer
17+
to maintain secrecy of this value once returned.
18+
- if location option is defined, then it deals with the regional parameters of the
19+
location
20+
21+
options:
22+
key:
23+
description:
24+
- the name of the parameter to look up in parameter Manager
25+
type: str
26+
required: True
27+
aliases:
28+
- name
29+
- parameter
30+
- parameter_id
31+
project:
32+
description:
33+
- The name of the google cloud project
34+
- defaults to OS env variable GCP_PROJECT if not present
35+
type: str
36+
location:
37+
description:
38+
- If provided, it defines the location of the regional parameter.
39+
type: str
40+
render_secret:
41+
description:
42+
- support for rendering secrets
43+
- defaults to false if not present
44+
type: bool
45+
auth_kind:
46+
description:
47+
- the type of authentication to use with Google Cloud (i.e. serviceaccount or machineaccount)
48+
- defaults to OS env variable GCP_AUTH_KIND if not present
49+
type: str
50+
version:
51+
description:
52+
- the version name of your parameter to retrieve
53+
type: str
54+
required: False
55+
service_account_email:
56+
description:
57+
- email associated with the service account
58+
- defaults to OS env variable GCP_SERVICE_ACCOUNT_EMAIL if not present
59+
type: str
60+
required: False
61+
service_account_file:
62+
description:
63+
- JSON Credential file obtained from Google Cloud
64+
- defaults to OS env variable GCP_SERVICE_ACCOUNT_FILE if not present
65+
- see https://cloud.google.com/iam/docs/service-account-creds for details
66+
type: str
67+
required: False
68+
service_account_info:
69+
description:
70+
- JSON Object representing the contents of a service_account_file obtained from Google Cloud
71+
- defaults to OS env variable GCP_SERVICE_ACCOUNT_INFO if not present
72+
type: dict
73+
required: False
74+
access_token:
75+
description:
76+
- support for GCP Access Token
77+
- defaults to OS env variable GCP_ACCESS_TOKEN if not present
78+
type: str
79+
required: False
80+
on_error:
81+
description:
82+
- how to handle errors
83+
- strict means raise an exception
84+
- warn means warn, and return none
85+
- ignore means just return none
86+
type: str
87+
required: False
88+
choices:
89+
- 'strict'
90+
- 'warn'
91+
- 'ignore'
92+
default: 'strict'
93+
scopes:
94+
description:
95+
- Authenticaiton scopes for Google parameter Manager
96+
type: list
97+
elements: str
98+
default: ["https://www.googleapis.com/auth/cloud-platform"]
99+
'''
100+
101+
102+
EXAMPLES = '''
103+
- name: Test parameter using env variables for credentials
104+
ansible.builtin.debug:
105+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', version='test_version') }}"
106+
107+
- name: Test parameter using explicit credentials
108+
ansible.builtin.debug:
109+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', version='test_version', project='project', auth_kind='serviceaccount',
110+
service_account_file='file.json') }}"
111+
112+
- name: Test getting specific version of a parameter
113+
ansible.builtin.debug:
114+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', version='test-version') }}"
115+
116+
- name: Test getting latest version of a parameter
117+
ansible.builtin.debug:
118+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key') }}"
119+
120+
- name: Test render specific version of a parameter
121+
ansible.builtin.debug:
122+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', version='test-version', render_secret=True) }}"
123+
124+
- name: Test render latest version of a parameter
125+
ansible.builtin.debug:
126+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', render_secret=True) }}"
127+
128+
- name: Test regional parameter using env variables for credentials
129+
ansible.builtin.debug:
130+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1', version='test_version') }}"
131+
132+
- name: Test regional parameter using explicit credentials
133+
ansible.builtin.debug:
134+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1', version='test_version', project='project',
135+
auth_kind='serviceaccount', service_account_file='file.json') }}"
136+
137+
- name: Test getting specific version of a regional parameter
138+
ansible.builtin.debug:
139+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1', version='test_version') }}"
140+
141+
- name: Test getting latest version of a regional parameter
142+
ansible.builtin.debug:
143+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1') }}"
144+
145+
- name: Test render specific version of a regional parameter
146+
ansible.builtin.debug:
147+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1', version='test_version', render_secret=True) }}"
148+
149+
- name: Test render latest version of a regional parameter
150+
ansible.builtin.debug:
151+
msg: "{{ lookup('google.cloud.gcp_parameter_manager', key='parameter_key', location='us-central1', render_secret=True) }}"
152+
'''
153+
154+
RETURN = '''
155+
_raw:
156+
description: the contents of the parameter requested (please use "no_log" to not expose this parameter)
157+
type: list
158+
elements: str
159+
'''
160+
161+
162+
################################################################################
163+
# Imports
164+
################################################################################
165+
166+
import os
167+
import base64
168+
169+
from ansible.plugins.lookup import LookupBase
170+
from ansible.errors import AnsibleError
171+
from ansible.utils.display import Display
172+
173+
try:
174+
import requests
175+
HAS_REQUESTS = True
176+
except ImportError:
177+
HAS_REQUESTS = False
178+
179+
try:
180+
from ansible_collections.google.cloud.plugins.module_utils.gcp_utils import (
181+
GcpSession,
182+
)
183+
HAS_GOOGLE_CLOUD_COLLECTION = True
184+
except ImportError:
185+
HAS_GOOGLE_CLOUD_COLLECTION = False
186+
187+
188+
class GcpLookupException(Exception):
189+
pass
190+
191+
192+
class GcpMockModule(object):
193+
def __init__(self, params):
194+
self.params = params
195+
196+
def fail_json(self, *args, **kwargs):
197+
raise AnsibleError(kwargs["msg"])
198+
199+
def raise_for_status(self, response):
200+
try:
201+
response.raise_for_status()
202+
except getattr(requests.exceptions, "RequestException"):
203+
self.fail_json(msg="GCP returned error: %s" % response.json())
204+
205+
206+
class LookupModule(LookupBase):
207+
def run(self, terms=None, variables=None, **kwargs):
208+
self._display = Display()
209+
if not HAS_GOOGLE_CLOUD_COLLECTION:
210+
raise AnsibleError(
211+
"""gcp_parameter lookup needs a supported version of the google.cloud
212+
collection installed. Use `ansible-galaxy collection install google.cloud`
213+
to install it"""
214+
)
215+
self.set_options(var_options=variables, direct=kwargs)
216+
params = {
217+
"key": self.get_option("key"),
218+
"location": self.get_option("location"),
219+
"version": self.get_option("version"),
220+
"access_token": self.get_option("access_token"),
221+
"scopes": self.get_option("scopes"),
222+
"render_secret": self.get_option("render_secret"),
223+
"on_error": self.get_option("on_error")
224+
}
225+
226+
params['name'] = params['key']
227+
228+
# support GCP_* env variables for some parameters
229+
for param in ["project", "auth_kind", "service_account_file", "service_account_info", "service_account_email", "access_token"]:
230+
params[param] = self.fallback_from_env(param)
231+
232+
self._display.vvv(msg=f"Module Parameters: {params}")
233+
fake_module = GcpMockModule(params)
234+
result = self.get_parameter(fake_module)
235+
return [base64.b64decode(result)]
236+
237+
def fallback_from_env(self, arg):
238+
if self.get_option(arg):
239+
return self.get_option(arg)
240+
else:
241+
env_name = f"GCP_{arg.upper()}"
242+
if env_name in os.environ:
243+
self.set_option(arg, os.environ[env_name])
244+
return self.get_option(arg)
245+
246+
def raise_error(self, module, msg):
247+
if module.params.get('on_error') == 'strict':
248+
raise GcpLookupException(msg)
249+
elif module.params.get('on_error') == 'warn':
250+
self._display.warning(msg)
251+
252+
return None
253+
254+
def get_latest_version(self, module, auth):
255+
url = (self.make_url_prefix(module) + "parameters/{name}/versions?orderBy=create_time desc&filter=disabled=false").format(
256+
**module.params
257+
)
258+
response = auth.get(url)
259+
self._display.vvv(msg=f"List Version Response: {response.status_code} for {response.request.url}: {response.json()}")
260+
if response.status_code != 200:
261+
self.raise_error(module, f"unable to list versions of parameter {response.status_code}")
262+
version_list = response.json()
263+
if "parameterVersions" in version_list and len(version_list["parameterVersions"]) > 0:
264+
# Extract name from the first index
265+
version_name = version_list["parameterVersions"][0]["name"]
266+
return version_name.split('/')[-1]
267+
else:
268+
self.raise_error(module, f"unable to list parameter versions via {response.request.url}: {response.json()}")
269+
270+
def get_parameter(self, module):
271+
auth = GcpSession(module, "parametermanager")
272+
273+
if module.params.get('project') is None:
274+
self.raise_error(module, "The project is required. Please specify the Google Cloud project to use.")
275+
276+
if module.params.get('version') == 'latest' or module.params.get('version') is None:
277+
module.params['version'] = self.get_latest_version(module, auth)
278+
279+
if module.params.get('render_secret') is None:
280+
module.params['render_secret'] = False
281+
282+
# there was an error listing parameter versions
283+
if module.params.get('version') is None:
284+
return ''
285+
286+
if module.params.get('render_secret') is not None:
287+
url = (self.make_url_prefix(module) + "parameters/{name}/versions/{version}:render").format(
288+
**module.params
289+
)
290+
else:
291+
url = (self.make_url_prefix(module) + "parameters/{name}/versions/{version}").format(
292+
**module.params
293+
)
294+
response = auth.get(url)
295+
self._display.vvv(msg=f"Response: {response.status_code} for {response.request.url}: {response.json()}")
296+
if response.status_code != 200:
297+
self.raise_error(module, f"Failed to lookup parameter value via {response.request.url} {response.status_code}")
298+
return ''
299+
300+
response_json = response.json()
301+
if module.params.get('render_secret') is not None:
302+
if 'renderedPayload' not in response_json:
303+
self.raise_error(module, "The parameter version is disabled or the response does not contain the 'renderedPayload' field.")
304+
return ''
305+
return response_json['renderedPayload']
306+
else:
307+
if 'payload' not in response_json or 'data' not in response_json['payload']:
308+
self.raise_error(module, "The parameter version is disabled or the response does not contain the 'data' field.")
309+
return ''
310+
return response_json['payload']['data']
311+
312+
def make_url_prefix(self, module):
313+
if module.params.get('location') and module.params.get('location') != 'global':
314+
return "https://parametermanager.{location}.rep.googleapis.com/v1/projects/{project}/locations/{location}/"
315+
return "https://parametermanager.googleapis.com/v1/projects/{project}/locations/global/"

0 commit comments

Comments
 (0)