Feat: Update GCP-SM Plugin for Empty Secret Creation
SUMMARY
The current plugin gcp_secret_manager.py will fail when fetching an empty secret from GCP Secrets Manager. It will also fail when attempting to create an empty secret since it requires creation WITH a value.
GCP Secret Manager API current allows the creation of an empty secret (no version or values). My changes are to support the fetching of an empty secret (not just the value) and creation of an empty secret while maintaining all current functionality of the module.
minor_changes: proper secret existence check; added empty secret initialization
ISSUE TYPE
- Feature Pull Request
COMPONENT NAME
gcp_secret_manager
ADDITIONAL INFORMATION
- Added new function check_secret_exists to properly determine if a secret exists in GCP Secret Manager
- Added ability to create a secret without an initial value by splitting the create_secret function into two separate functions - create_secret_with_value and create_secret_without_value
- Improved logic for handling existence of secrets and their versions to better support empty secrets
BEFORE CHANGES
# Example Task:
- name: Ensure the Secret in GCP-SM Exists
google.cloud.gcp_secret_manager:
name: <SECRET_NAME>
project: "<PROJECT_NAME>"
auth_kind: application
value: "SECRET"
labels:
test: "true"
# OUTPUT If the secret already exists but doesn't have a value OR if the secret doesn't exist and no value is specified
"msg": "secret '<SECRET_NAME>' not present in '<PROJECT_NAME>' and no value for the secret is provided"
# OUTPUT if the secret already exists AND a value is provided:
"msg": "GCP returned error: {'error': {'code': 409, 'message': 'Secret [projects/<PROJECT_ID>/secrets/<SECRET_NAME>] already exists.', 'status': 'ALREADY_EXISTS'
The 409 is due to the fact that the module currently attempts to fetch if the VERSION (with its value) exists, which returns FALSE since its an empty secret. This means when it goes to "update" a pre-existing secret, it 409's since its attempting to create a resource that already exists. I update this so it fetches if the SECRET exists, and then we handle updating / setting the version information outside the creation depending on if a secret exists or not.
AFTER CHANGES
# Creation of empty secret task example:
- name: Ensure the Secret in GCP-SM Exists
google.cloud.gcp_secret_manager:
name: <SECRET_NAME>
project: "<PROJECT_NAME>"
auth_kind: application
labels:
test: "true"
# RESULT:
changed: [<HOSTNAME>] => {
"changed": true,
"invocation": {
"module_args": {
"access_token": null,
"auth_kind": "application",
"calc_version": "latest",
"env_type": null,
"labels": {
"test": "true"
},
"location": null,
"name": "<SECRET_NAME>",
"project": "<PROJECT_NAME>",
"return_value": true,
"scopes": [
"https://www.googleapis.com/auth/cloud-platform"
],
"service_account_contents": null,
"service_account_email": null,
"service_account_file": null,
"state": "present",
"value": null,
"version": "latest"
}
},
"msg": "Secret '<SECRET_NAME>' created without a value",
"name": "<SECRET_NAME>"
}
# Update of pre-existing secret with initial value (version) task example:
- name: Ensure the Secret in GCP-SM Exists
google.cloud.gcp_secret_manager:
name: <SECRET_NAME>
project: "<PROJECT_NAME>"
auth_kind: application
value: "SECRET"
labels:
test: "true"
# RESULT:
changed: [<HOSTNAME>] => {
"changed": true,
"createTime": "2025-08-13T22:57:54.111655Z",
"etag": "\"163c47164334a7\"",
"invocation": {
"module_args": {
"access_token": null,
"auth_kind": "application",
"calc_version": "latest",
"env_type": null,
"labels": {
"test": "true"
},
"location": null,
"name": "<SECRET_NAME>",
"project": "<PROJECT_NAME",
"return_value": true,
"scopes": [
"https://www.googleapis.com/auth/cloud-platform"
],
"service_account_contents": null,
"service_account_email": null,
"service_account_file": null,
"state": "present",
"value": "SECRET",
"version": "latest"
}
},
"name": "<SECRET_NAME>",
"replicationStatus": {
"automatic": {}
},
"state": "ENABLED",
"status_code": 200,
"url": "https://secretmanager.googleapis.com/v1/projects/<PROJECT_NAME>/secrets/<SECRET_NAME>:addVersion",
"version": "1"
}
Hello @Michael-Burke I see that you have a sort of test case already, would you mind adding to/modifying the integration tests? The test entrypoint is in tests/integration/targets/gcp_secret_manager/tasks/main.yml
@thekad Great catch! Added and updated to include the new test cases of:
- Creation of an empty secret
- Creation of an empty secret that already exists
- Deletion of an empty secret
- Updated the language to state when a version + value included in creation
hello @Michael-Burke apologies in advance but quick question: are you using AI to write this feature? Asking because we don't have any policy/guidelines for AI assisted contributions (yet) and I believe that would be something worth considering first cc @SirGitsalot
hello @thekad, I did use AI to assist, but mainly its prediction and autocomplete feature. The python isn't that complicated so I didn't ask it to write it for me. Happy to re-write / implement if that'll be an issue.
@Michael-Burke I don't think there is a problem, we just don't have a contribution policy for such cases, and we should clear those up before merging is all. I'll leave that to @SirGitsalot as he's the project admin.
The policy is that a PR "does need to be a contributor’s original creation" but it's OK to use "coding assistance tools" so this PR is fine.
@Michael-Burke there's a failure in the sanity tests, you can quickly run these in your workstation to make sure they will pass e.g. ansible-test sanity --python-interpreter $(which python3) gcp_secret_manager
@thekad Thank you! I've updated the code to fix the failing sanity tests. Please let me know If there is anything else that's failing:
❯ ansible-test sanity --python-interpreter $(which python3) gcp_secret_manager
Running sanity test "action-plugin-docs"
Running sanity test "ansible-doc"
Running sanity test "changelog"
Running sanity test "compile" on Python 3.13
Running sanity test "empty-init"
Running sanity test "ignores"
Running sanity test "import" on Python 3.13
Running sanity test "line-endings"
Running sanity test "no-assert"
Running sanity test "no-get-exception"
Running sanity test "no-illegal-filenames"
Running sanity test "no-smart-quotes"
Running sanity test "pep8"
Running sanity test "pslint"
Running sanity test "pylint"
Running sanity test "replace-urlopen"
Running sanity test "runtime-metadata"
Running sanity test "shebang"
Running sanity test "shellcheck"
Running sanity test "symlinks"
Running sanity test "use-argspec-type-path"
Running sanity test "use-compat-six"
Running sanity test "validate-modules"
Running sanity test "yamllint"
@Michael-Burke can you rebase please?
@Michael-Burke can you rebase please?
@thekad rebase should be complete; I also re-ran the sanity tests which are still passing. Let me know if there is anything else I can help with or do. Thank you!
➜ ansible-test sanity --python-interpreter $(which python3) gcp_secret_manager
Running sanity test "action-plugin-docs"
Running sanity test "ansible-doc"
Running sanity test "changelog"
Running sanity test "compile" on Python 3.13
Running sanity test "empty-init"
Running sanity test "ignores"
Running sanity test "import" on Python 3.13
Running sanity test "line-endings"
Running sanity test "no-assert"
Running sanity test "no-get-exception"
Running sanity test "no-illegal-filenames"
Running sanity test "no-smart-quotes"
Running sanity test "pep8"
Running sanity test "pslint"
Running sanity test "pylint"
Running sanity test "replace-urlopen"
Running sanity test "runtime-metadata"
Running sanity test "shebang"
Running sanity test "shellcheck"
Running sanity test "symlinks"
Running sanity test "use-argspec-type-path"
Running sanity test "use-compat-six"
Running sanity test "validate-modules"
Running sanity test "yamllint"
@Michael-Burke sanity tests look good, but integration tests are failing with this exception:
TASK [gcp_secret_manager : Create an empty secret that already exists] *********
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: TypeError: argument of type 'NoneType' is not iterable
fatal: [testhost]: FAILED! => {"changed": false, "module_stderr": "Traceback (most recent call last):\n File \"<stdin>\", line 121, in <module>\n File \"<stdin>\", line 113, in _ansiballz_main\n File \"<stdin>\", line 61, in invoke_module\n File \"<frozen runpy>\", line 226, in run_module\n File \"<frozen runpy>\", line 98, in _run_module_code\n File \"<frozen runpy>\", line 88, in _run_code\n File \"/tmp/ansible_google.cloud.gcp_secret_manager_payload_hr_2_e50/ansible_google.cloud.gcp_secret_manager_payload.zip/ansible_collections/google/cloud/plugins/modules/gcp_secret_manager.py\", line 579, in <module>\n File \"/tmp/ansible_google.cloud.gcp_secret_manager_payload_hr_2_e50/ansible_google.cloud.gcp_secret_manager_payload.zip/ansible_collections/google/cloud/plugins/modules/gcp_secret_manager.py\", line 555, in main\nTypeError: argument of type 'NoneType' is not iterable\n", "module_stdout": "", "msg": "MODULE FAILURE: No start of json char found\nSee stdout/stderr for the exact error", "rc": 1}
PLAY RECAP *********************************************************************
testhost : ok=5 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
If you're curious, you can run the integration tests yourself:
- edit/create tests/integration/cloud-config-gcp.ini
- run ansible-test integration gcp_secret_manager
(or you could build your own integration test playbook and then amend the integration tests)