google.cloud icon indicating copy to clipboard operation
google.cloud copied to clipboard

Feat: Update GCP-SM Plugin for Empty Secret Creation

Open Michael-Burke opened this issue 5 months ago • 11 comments

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"
}

Michael-Burke avatar Aug 13 '25 23:08 Michael-Burke

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 avatar Aug 18 '25 15:08 thekad

@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

Michael-Burke avatar Aug 18 '25 15:08 Michael-Burke

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

thekad avatar Aug 18 '25 19:08 thekad

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 avatar Aug 19 '25 14:08 Michael-Burke

@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.

thekad avatar Aug 19 '25 14:08 thekad

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.

SirGitsalot avatar Aug 19 '25 22:08 SirGitsalot

@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 avatar Aug 21 '25 01:08 thekad

@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 avatar Aug 22 '25 15:08 Michael-Burke

@Michael-Burke can you rebase please?

thekad avatar Nov 12 '25 22:11 thekad

@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 avatar Nov 12 '25 23:11 Michael-Burke

@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:

  1. edit/create tests/integration/cloud-config-gcp.ini
  2. run ansible-test integration gcp_secret_manager

(or you could build your own integration test playbook and then amend the integration tests)

thekad avatar Nov 14 '25 19:11 thekad