Fixing additional tests

Fixing some of the existing failing tests in the CI process.

Specifically:

- gcp_appengine_firewall_rule: modified validation to support the
  default firewall rule
- gcp_cloudfunctions_cloud_function: bootstrapping the required GS
  bucket and files for creating a valid cloud function.
  - Slight update to the functionality, which now requires a runtime
    specified for new functions.
-
This commit is contained in:
Yusuke Tsutsumi 2022-10-08 17:36:39 +00:00
parent c5723b214f
commit 0387ad3c17
10 changed files with 203 additions and 134 deletions

View file

@ -25,9 +25,13 @@ __metaclass__ = type
# Documentation # Documentation
################################################################################ ################################################################################
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ["preview"], 'supported_by': 'community'} ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}
DOCUMENTATION = ''' DOCUMENTATION = """
--- ---
module: gcp_cloudfunctions_cloud_function module: gcp_cloudfunctions_cloud_function
description: description:
@ -69,8 +73,8 @@ options:
type: str type: str
runtime: runtime:
description: description:
- The runtime in which the function is going to run. If empty, defaults to Node.js - The runtime in which to run the function. Required when deploying a new function,
6. optional when updating an existing function.
required: false required: false
type: str type: str
timeout: timeout:
@ -195,9 +199,9 @@ options:
- This should not be set unless you know what you're doing. - This should not be set unless you know what you're doing.
- This only alters the User Agent string for any API requests. - This only alters the User Agent string for any API requests.
type: str type: str
''' """
EXAMPLES = ''' EXAMPLES = """
- name: create a cloud function - name: create a cloud function
google.cloud.gcp_cloudfunctions_cloud_function: google.cloud.gcp_cloudfunctions_cloud_function:
name: test_object name: test_object
@ -209,9 +213,9 @@ EXAMPLES = '''
auth_kind: serviceaccount auth_kind: serviceaccount
service_account_file: "/tmp/auth.pem" service_account_file: "/tmp/auth.pem"
state: present state: present
''' """
RETURN = ''' RETURN = """
name: name:
description: description:
- A user-defined name of the function. Function names must be unique globally and - A user-defined name of the function. Function names must be unique globally and
@ -353,7 +357,7 @@ trigger_http:
- Use HTTP to trigger this function. - Use HTTP to trigger this function.
returned: success returned: success
type: bool type: bool
''' """
################################################################################ ################################################################################
# Imports # Imports
@ -381,43 +385,50 @@ def main():
module = GcpModule( module = GcpModule(
argument_spec=dict( argument_spec=dict(
state=dict(default='present', choices=['present', 'absent'], type='str'), state=dict(default="present", choices=["present", "absent"], type="str"),
name=dict(required=True, type='str'), name=dict(required=True, type="str"),
description=dict(type='str'), description=dict(type="str"),
entry_point=dict(type='str'), entry_point=dict(type="str"),
runtime=dict(type='str'), runtime=dict(type="str"),
timeout=dict(type='str'), timeout=dict(type="str"),
available_memory_mb=dict(type='int'), available_memory_mb=dict(type="int"),
labels=dict(type='dict'), labels=dict(type="dict"),
environment_variables=dict(type='dict'), environment_variables=dict(type="dict"),
source_archive_url=dict(type='str'), source_archive_url=dict(type="str"),
source_upload_url=dict(type='str'), source_upload_url=dict(type="str"),
source_repository=dict(type='dict', options=dict(url=dict(required=True, type='str'))), source_repository=dict(
https_trigger=dict(type='dict', options=dict()), type="dict", options=dict(url=dict(required=True, type="str"))
event_trigger=dict(
type='dict', options=dict(event_type=dict(required=True, type='str'), resource=dict(required=True, type='str'), service=dict(type='str'))
), ),
location=dict(required=True, type='str'), https_trigger=dict(type="dict", options=dict()),
trigger_http=dict(type='bool'), event_trigger=dict(
type="dict",
options=dict(
event_type=dict(required=True, type="str"),
resource=dict(required=True, type="str"),
service=dict(type="str"),
),
),
location=dict(required=True, type="str"),
trigger_http=dict(type="bool"),
) )
) )
if not module.params['scopes']: if not module.params["scopes"]:
module.params['scopes'] = ['https://www.googleapis.com/auth/cloud-platform'] module.params["scopes"] = ["https://www.googleapis.com/auth/cloud-platform"]
state = module.params['state'] state = module.params["state"]
fetch = fetch_resource(module, self_link(module)) fetch = fetch_resource(module, self_link(module))
changed = False changed = False
# Need to set triggerHttps to {} if boolean true. # Need to set triggerHttps to {} if boolean true.
if fetch and fetch.get('httpsTrigger') and module.params['trigger_http']: if fetch and fetch.get("httpsTrigger") and module.params["trigger_http"]:
module.params['https_trigger'] = fetch.get('httpsTrigger') module.params["https_trigger"] = fetch.get("httpsTrigger")
elif module.params['trigger_http']: elif module.params["trigger_http"]:
module.params['https_trigger'] = {} module.params["https_trigger"] = {}
if fetch: if fetch:
if state == 'present': if state == "present":
if is_different(module, fetch): if is_different(module, fetch):
update(module, self_link(module), fetch) update(module, self_link(module), fetch)
fetch = fetch_resource(module, self_link(module)) fetch = fetch_resource(module, self_link(module))
@ -427,101 +438,115 @@ def main():
fetch = {} fetch = {}
changed = True changed = True
else: else:
if state == 'present': if state == "present":
fetch = create(module, collection(module)) fetch = create(module, collection(module))
changed = True changed = True
else: else:
fetch = {} fetch = {}
fetch.update({'changed': changed}) fetch.update({"changed": changed})
module.exit_json(**fetch) module.exit_json(**fetch)
def create(module, link): def create(module, link):
auth = GcpSession(module, 'cloudfunctions') auth = GcpSession(module, "cloudfunctions")
return wait_for_operation(module, auth.post(link, resource_to_request(module))) return wait_for_operation(module, auth.post(link, resource_to_request(module)))
def update(module, link, fetch): def update(module, link, fetch):
auth = GcpSession(module, 'cloudfunctions') auth = GcpSession(module, "cloudfunctions")
params = {'updateMask': updateMask(resource_to_request(module), response_to_hash(module, fetch))} params = {
"updateMask": updateMask(
resource_to_request(module), response_to_hash(module, fetch)
)
}
request = resource_to_request(module) request = resource_to_request(module)
del request['name'] del request["name"]
return wait_for_operation(module, auth.put(link, request, params=params)) return wait_for_operation(module, auth.put(link, request, params=params))
def updateMask(request, response): def updateMask(request, response):
update_mask = [] update_mask = []
if request.get('name') != response.get('name'): if request.get("name") != response.get("name"):
update_mask.append('name') update_mask.append("name")
if request.get('description') != response.get('description'): if request.get("description") != response.get("description"):
update_mask.append('description') update_mask.append("description")
if request.get('entryPoint') != response.get('entryPoint'): if request.get("entryPoint") != response.get("entryPoint"):
update_mask.append('entryPoint') update_mask.append("entryPoint")
if request.get('runtime') != response.get('runtime'): if request.get("runtime") != response.get("runtime"):
update_mask.append('runtime') update_mask.append("runtime")
if request.get('timeout') != response.get('timeout'): if request.get("timeout") != response.get("timeout"):
update_mask.append('timeout') update_mask.append("timeout")
if request.get('availableMemoryMb') != response.get('availableMemoryMb'): if request.get("availableMemoryMb") != response.get("availableMemoryMb"):
update_mask.append('availableMemoryMb') update_mask.append("availableMemoryMb")
if request.get('labels') != response.get('labels'): if request.get("labels") != response.get("labels"):
update_mask.append('labels') update_mask.append("labels")
if request.get('environmentVariables') != response.get('environmentVariables'): if request.get("environmentVariables") != response.get("environmentVariables"):
update_mask.append('environmentVariables') update_mask.append("environmentVariables")
if request.get('sourceArchiveUrl') != response.get('sourceArchiveUrl'): if request.get("sourceArchiveUrl") != response.get("sourceArchiveUrl"):
update_mask.append('sourceArchiveUrl') update_mask.append("sourceArchiveUrl")
if request.get('sourceUploadUrl') != response.get('sourceUploadUrl'): if request.get("sourceUploadUrl") != response.get("sourceUploadUrl"):
update_mask.append('sourceUploadUrl') update_mask.append("sourceUploadUrl")
if request.get('sourceRepository') != response.get('sourceRepository'): if request.get("sourceRepository") != response.get("sourceRepository"):
update_mask.append('sourceRepository') update_mask.append("sourceRepository")
if request.get('httpsTrigger') != response.get('httpsTrigger'): if request.get("httpsTrigger") != response.get("httpsTrigger"):
update_mask.append('httpsTrigger') update_mask.append("httpsTrigger")
if request.get('eventTrigger') != response.get('eventTrigger'): if request.get("eventTrigger") != response.get("eventTrigger"):
update_mask.append('eventTrigger') update_mask.append("eventTrigger")
if request.get('location') != response.get('location'): if request.get("location") != response.get("location"):
update_mask.append('location') update_mask.append("location")
if request.get('trigger_http') != response.get('trigger_http'): if request.get("trigger_http") != response.get("trigger_http"):
update_mask.append('trigger_http') update_mask.append("trigger_http")
return ','.join(update_mask) return ",".join(update_mask)
def delete(module, link): def delete(module, link):
auth = GcpSession(module, 'cloudfunctions') auth = GcpSession(module, "cloudfunctions")
return wait_for_operation(module, auth.delete(link)) return wait_for_operation(module, auth.delete(link))
def resource_to_request(module): def resource_to_request(module):
request = { request = {
u'name': name_pattern(module.params.get('name'), module), "name": name_pattern(module.params.get("name"), module),
u'description': module.params.get('description'), "description": module.params.get("description"),
u'entryPoint': module.params.get('entry_point'), "entryPoint": module.params.get("entry_point"),
u'runtime': module.params.get('runtime'), "runtime": module.params.get("runtime"),
u'timeout': module.params.get('timeout'), "timeout": module.params.get("timeout"),
u'availableMemoryMb': module.params.get('available_memory_mb'), "availableMemoryMb": module.params.get("available_memory_mb"),
u'labels': module.params.get('labels'), "labels": module.params.get("labels"),
u'environmentVariables': module.params.get('environment_variables'), "environmentVariables": module.params.get("environment_variables"),
u'sourceArchiveUrl': module.params.get('source_archive_url'), "sourceArchiveUrl": module.params.get("source_archive_url"),
u'sourceUploadUrl': module.params.get('source_upload_url'), "sourceUploadUrl": module.params.get("source_upload_url"),
u'sourceRepository': CloudFunctionSourcerepository(module.params.get('source_repository', {}), module).to_request(), "sourceRepository": CloudFunctionSourcerepository(
u'httpsTrigger': CloudFunctionHttpstrigger(module.params.get('https_trigger', {}), module).to_request(), module.params.get("source_repository", {}), module
u'eventTrigger': CloudFunctionEventtrigger(module.params.get('event_trigger', {}), module).to_request(), ).to_request(),
"httpsTrigger": CloudFunctionHttpstrigger(
module.params.get("https_trigger", {}), module
).to_request(),
"eventTrigger": CloudFunctionEventtrigger(
module.params.get("event_trigger", {}), module
).to_request(),
} }
request = encode_request(request, module) request = encode_request(request, module)
return request return request
def fetch_resource(module, link, allow_not_found=True): def fetch_resource(module, link, allow_not_found=True):
auth = GcpSession(module, 'cloudfunctions') auth = GcpSession(module, "cloudfunctions")
return return_if_object(module, auth.get(link), allow_not_found) return return_if_object(module, auth.get(link), allow_not_found)
def self_link(module): def self_link(module):
return "https://cloudfunctions.googleapis.com/v1/projects/{project}/locations/{location}/functions/{name}".format(**module.params) return "https://cloudfunctions.googleapis.com/v1/projects/{project}/locations/{location}/functions/{name}".format(
**module.params
)
def collection(module): def collection(module):
return "https://cloudfunctions.googleapis.com/v1/projects/{project}/locations/{location}/functions".format(**module.params) return "https://cloudfunctions.googleapis.com/v1/projects/{project}/locations/{location}/functions".format(
**module.params
)
def return_if_object(module, response, allow_not_found=False): def return_if_object(module, response, allow_not_found=False):
@ -536,11 +561,11 @@ def return_if_object(module, response, allow_not_found=False):
try: try:
module.raise_for_status(response) module.raise_for_status(response)
result = response.json() result = response.json()
except getattr(json.decoder, 'JSONDecodeError', ValueError): except getattr(json.decoder, "JSONDecodeError", ValueError):
module.fail_json(msg="Invalid JSON response with error: %s" % response.text) module.fail_json(msg="Invalid JSON response with error: %s" % response.text)
if navigate_hash(result, ['error', 'errors']): if navigate_hash(result, ["error", "errors"]):
module.fail_json(msg=navigate_hash(result, ['error', 'errors'])) module.fail_json(msg=navigate_hash(result, ["error", "errors"]))
return result return result
@ -567,23 +592,29 @@ def is_different(module, response):
# This is for doing comparisons with Ansible's current parameters. # This is for doing comparisons with Ansible's current parameters.
def response_to_hash(module, response): def response_to_hash(module, response):
return { return {
u'name': response.get(u'name'), "name": response.get("name"),
u'description': response.get(u'description'), "description": response.get("description"),
u'status': response.get(u'status'), "status": response.get("status"),
u'entryPoint': response.get(u'entryPoint'), "entryPoint": response.get("entryPoint"),
u'runtime': response.get(u'runtime'), "runtime": response.get("runtime"),
u'timeout': response.get(u'timeout'), "timeout": response.get("timeout"),
u'availableMemoryMb': response.get(u'availableMemoryMb'), "availableMemoryMb": response.get("availableMemoryMb"),
u'serviceAccountEmail': response.get(u'serviceAccountEmail'), "serviceAccountEmail": response.get("serviceAccountEmail"),
u'updateTime': response.get(u'updateTime'), "updateTime": response.get("updateTime"),
u'versionId': response.get(u'versionId'), "versionId": response.get("versionId"),
u'labels': response.get(u'labels'), "labels": response.get("labels"),
u'environmentVariables': response.get(u'environmentVariables'), "environmentVariables": response.get("environmentVariables"),
u'sourceArchiveUrl': response.get(u'sourceArchiveUrl'), "sourceArchiveUrl": response.get("sourceArchiveUrl"),
u'sourceUploadUrl': response.get(u'sourceUploadUrl'), "sourceUploadUrl": response.get("sourceUploadUrl"),
u'sourceRepository': CloudFunctionSourcerepository(response.get(u'sourceRepository', {}), module).from_response(), "sourceRepository": CloudFunctionSourcerepository(
u'httpsTrigger': CloudFunctionHttpstrigger(response.get(u'httpsTrigger', {}), module).from_response(), response.get("sourceRepository", {}), module
u'eventTrigger': CloudFunctionEventtrigger(response.get(u'eventTrigger', {}), module).from_response(), ).from_response(),
"httpsTrigger": CloudFunctionHttpstrigger(
response.get("httpsTrigger", {}), module
).from_response(),
"eventTrigger": CloudFunctionEventtrigger(
response.get("eventTrigger", {}), module
).from_response(),
} }
@ -594,7 +625,9 @@ def name_pattern(name, module):
regex = r"projects/.*/locations/.*/functions/.*" regex = r"projects/.*/locations/.*/functions/.*"
if not re.match(regex, name): if not re.match(regex, name):
name = "projects/{project}/locations/{location}/functions/{name}".format(**module.params) name = "projects/{project}/locations/{location}/functions/{name}".format(
**module.params
)
return name return name
@ -612,20 +645,20 @@ def wait_for_operation(module, response):
op_result = return_if_object(module, response) op_result = return_if_object(module, response)
if op_result is None: if op_result is None:
return {} return {}
status = navigate_hash(op_result, ['done']) status = navigate_hash(op_result, ["done"])
wait_done = wait_for_completion(status, op_result, module) wait_done = wait_for_completion(status, op_result, module)
raise_if_errors(wait_done, ['error'], module) raise_if_errors(wait_done, ["error"], module)
return navigate_hash(wait_done, ['response']) return navigate_hash(wait_done, ["response"])
def wait_for_completion(status, op_result, module): def wait_for_completion(status, op_result, module):
op_id = navigate_hash(op_result, ['name']) op_id = navigate_hash(op_result, ["name"])
op_uri = async_op_url(module, {'op_id': op_id}) op_uri = async_op_url(module, {"op_id": op_id})
while not status: while not status:
raise_if_errors(op_result, ['error'], module) raise_if_errors(op_result, ["error"], module)
time.sleep(1.0) time.sleep(1.0)
op_result = fetch_resource(module, op_uri, False) op_result = fetch_resource(module, op_uri, False)
status = navigate_hash(op_result, ['done']) status = navigate_hash(op_result, ["done"])
return op_result return op_result
@ -641,8 +674,8 @@ def encode_request(request, module):
if v or v is False: if v or v is False:
return_vals[k] = v return_vals[k] = v
if module.params['trigger_http'] and not return_vals.get('httpsTrigger'): if module.params["trigger_http"] and not return_vals.get("httpsTrigger"):
return_vals['httpsTrigger'] = {} return_vals["httpsTrigger"] = {}
return return_vals return return_vals
@ -656,10 +689,10 @@ class CloudFunctionSourcerepository(object):
self.request = {} self.request = {}
def to_request(self): def to_request(self):
return remove_nones_from_dict({u'url': self.request.get('url')}) return remove_nones_from_dict({"url": self.request.get("url")})
def from_response(self): def from_response(self):
return remove_nones_from_dict({u'url': self.request.get(u'url')}) return remove_nones_from_dict({"url": self.request.get("url")})
class CloudFunctionHttpstrigger(object): class CloudFunctionHttpstrigger(object):
@ -687,14 +720,22 @@ class CloudFunctionEventtrigger(object):
def to_request(self): def to_request(self):
return remove_nones_from_dict( return remove_nones_from_dict(
{u'eventType': self.request.get('event_type'), u'resource': self.request.get('resource'), u'service': self.request.get('service')} {
"eventType": self.request.get("event_type"),
"resource": self.request.get("resource"),
"service": self.request.get("service"),
}
) )
def from_response(self): def from_response(self):
return remove_nones_from_dict( return remove_nones_from_dict(
{u'eventType': self.request.get(u'eventType'), u'resource': self.request.get(u'resource'), u'service': self.request.get(u'service')} {
"eventType": self.request.get("eventType"),
"resource": self.request.get("resource"),
"service": self.request.get("service"),
}
) )
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -11,6 +11,7 @@ SERVICE_ACCOUNT_NAME="${2}"
SERVICE_LIST=( SERVICE_LIST=(
"appengine" "appengine"
"bigtable" "bigtable"
"cloudbuild.googleapis.com"
"cloudfunctions" "cloudfunctions"
"cloudkms.googleapis.com" "cloudkms.googleapis.com"
"cloudresourcemanager.googleapis.com" "cloudresourcemanager.googleapis.com"
@ -50,11 +51,16 @@ if ! gcloud app describe --project="$PROJECT_ID" > /dev/null; then
gcloud app create --project="$PROJECT_ID" --region=us-central gcloud app create --project="$PROJECT_ID" --region=us-central
fi fi
# create and upload cloud function for testing
BUCKET_NAME="gs://${PROJECT_ID}-ansible-testing"
# Add bindings if ! gcloud storage buckets describe "${BUCKET_NAME}" > /dev/null; then
gcloud storage buckets create "gs://${PROJECT_ID}-ansible-testing" --project="${PROJECT_ID}"
fi
gsutil cp ./test-fixtures/cloud-function.zip "gs://${PROJECT_ID}-ansible-testing"
# roles/storage.objectAdmin
# The following is hard to automate, so echo # The following is hard to automate, so echo
echo "Done! It may take up to 10 minutes for some of the changes to fully propagate." echo "Done! It may take up to 10 minutes for some of the changes to fully propagate."

View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Build the cloud function zip file,
# in the desired cloud function source format.
if [ -f ../cloud-function.zip ]; then
rm ../cloud-function.zip
fi
zip ../cloud-function.zip *

View file

@ -0,0 +1,9 @@
import functions_framework
# Register an HTTP function with the Functions Framework
@functions_framework.http
def helloGET(request):
# Your code here
# Return an HTTP response
return "OK"

View file

@ -0,0 +1 @@
functions-framework==3.*

Binary file not shown.

View file

@ -1,2 +1 @@
cloud/gcp cloud/gcp
unsupported

View file

@ -90,7 +90,9 @@
- name: verify that command succeeded - name: verify that command succeeded
assert: assert:
that: that:
- results['resources'] | length == 0 # there is a default firewall rule that cannot be
# deleted, so the length should be 1.
- results['resources'] | length == 1
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
- name: delete a firewall rule that does not exist - name: delete a firewall rule that does not exist
google.cloud.gcp_appengine_firewall_rule: google.cloud.gcp_appengine_firewall_rule:

View file

@ -1,2 +1 @@
cloud/gcp cloud/gcp
unsupported

View file

@ -18,9 +18,10 @@
name: "{{ resource_name }}" name: "{{ resource_name }}"
location: us-central1 location: us-central1
entry_point: helloGET entry_point: helloGET
source_archive_url: gs://ansible-cloudfunctions-bucket/function.zip source_archive_url: gs://{{ gcp_project }}-ansible-testing/cloud-function.zip
trigger_http: 'true' trigger_http: 'true'
project: "{{ gcp_project }}" project: "{{ gcp_project }}"
runtime: "python310"
auth_kind: "{{ gcp_cred_kind }}" auth_kind: "{{ gcp_cred_kind }}"
service_account_file: "{{ gcp_cred_file }}" service_account_file: "{{ gcp_cred_file }}"
state: absent state: absent
@ -30,9 +31,10 @@
name: "{{ resource_name }}" name: "{{ resource_name }}"
location: us-central1 location: us-central1
entry_point: helloGET entry_point: helloGET
source_archive_url: gs://ansible-cloudfunctions-bucket/function.zip source_archive_url: gs://{{ gcp_project }}-ansible-testing/cloud-function.zip
trigger_http: 'true' trigger_http: 'true'
project: "{{ gcp_project }}" project: "{{ gcp_project }}"
runtime: "python310"
auth_kind: "{{ gcp_cred_kind }}" auth_kind: "{{ gcp_cred_kind }}"
service_account_file: "{{ gcp_cred_file }}" service_account_file: "{{ gcp_cred_file }}"
state: present state: present
@ -60,10 +62,13 @@
name: "{{ resource_name }}" name: "{{ resource_name }}"
location: us-central1 location: us-central1
entry_point: helloGET entry_point: helloGET
source_archive_url: gs://ansible-cloudfunctions-bucket/function.zip source_archive_url: gs://{{ gcp_project }}-ansible-testing/cloud-function.zip
trigger_http: 'true' trigger_http: 'true'
project: "{{ gcp_project }}" project: "{{ gcp_project }}"
auth_kind: "{{ gcp_cred_kind }}" auth_kind: "{{ gcp_cred_kind }}"
# runtime is not sent as it is optional for
# existing functions.
# runtime: "python310"
service_account_file: "{{ gcp_cred_file }}" service_account_file: "{{ gcp_cred_file }}"
state: present state: present
register: result register: result
@ -77,7 +82,7 @@
name: "{{ resource_name }}" name: "{{ resource_name }}"
location: us-central1 location: us-central1
entry_point: helloGET entry_point: helloGET
source_archive_url: gs://ansible-cloudfunctions-bucket/function.zip source_archive_url: gs://{{ gcp_project }}-ansible-testing/cloud-function.zip
trigger_http: 'true' trigger_http: 'true'
project: "{{ gcp_project }}" project: "{{ gcp_project }}"
auth_kind: "{{ gcp_cred_kind }}" auth_kind: "{{ gcp_cred_kind }}"
@ -107,7 +112,7 @@
name: "{{ resource_name }}" name: "{{ resource_name }}"
location: us-central1 location: us-central1
entry_point: helloGET entry_point: helloGET
source_archive_url: gs://ansible-cloudfunctions-bucket/function.zip source_archive_url: gs://{{ gcp_project }}-ansible-testing/cloud-function.zip
trigger_http: 'true' trigger_http: 'true'
project: "{{ gcp_project }}" project: "{{ gcp_project }}"
auth_kind: "{{ gcp_cred_kind }}" auth_kind: "{{ gcp_cred_kind }}"