From 2b37e8431dcbd04dd5bdc3af6a20177c71ece858 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Thu, 15 Jan 2026 18:32:20 +0530 Subject: [PATCH 01/24] feat(secretmanager): Adding tags samples --- secretmanager/snippets/detach_tag_binding.py | 85 ++++++++++++++ secretmanager/snippets/list_tag_bindings.py | 81 ++++++++++++++ .../regional_samples/detach_regional_tag.py | 105 ++++++++++++++++++ .../list_regional_secret_tag_bindings.py | 91 +++++++++++++++ .../regional_samples/snippets_test.py | 49 +++++++- secretmanager/snippets/snippets_test.py | 52 ++++++++- 6 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 secretmanager/snippets/detach_tag_binding.py create mode 100644 secretmanager/snippets/list_tag_bindings.py create mode 100644 secretmanager/snippets/regional_samples/detach_regional_tag.py create mode 100644 secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py diff --git a/secretmanager/snippets/detach_tag_binding.py b/secretmanager/snippets/detach_tag_binding.py new file mode 100644 index 00000000000..4afa9153566 --- /dev/null +++ b/secretmanager/snippets/detach_tag_binding.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +import argparse + +# [START secretmanager_detach_tag_binding] +from google.cloud import resourcemanager_v3 + + +def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: + """ + Detaches a tag value from a secret. + + Args: + project_id (str): The project ID where the secret exists. + secret_id (str): The ID of the secret from which to detach the tag. + tag_value (str): The tag value to detach (e.g., "tagValues/123456789012"). + + Example: + # Detach a tag value from a secret + detach_tag("my-project", "my-secret", "tagValues/123456789012") + """ + # Create the Resource Manager client. + client = resourcemanager_v3.TagBindingsClient() + + # Build the resource name of the parent secret. + secret_name = f"projects/{project_id}/secrets/{secret_id}" + parent = f"//secretmanager.googleapis.com/{secret_name}" + + # Find the binding name for the given tag value + binding_name = None + request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + + for binding in client.list_tag_bindings(request=request): + if binding.tag_value == tag_value: + binding_name = binding.name + break + + if binding_name is None: + print(f"Tag binding for value {tag_value} not found on {secret_name}.") + return + + # Delete the tag binding + request = resourcemanager_v3.DeleteTagBindingRequest(name=binding_name) + operation = client.delete_tag_binding(request=request) + + # Wait for the operation to complete + operation.result() + + print(f"Detached tag value {tag_value} from {secret_name}") + + +# [END secretmanager_detach_tag_binding] + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument( + "secret_id", help="id of the secret to detach tag from" + ) + parser.add_argument( + "tag_value", + help="tag value to detach (e.g., 'tagValues/123456789012')", + ) + args = parser.parse_args() + + detach_tag(args.project_id, args.secret_id, args.tag_value) diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py new file mode 100644 index 00000000000..ea04fed9c16 --- /dev/null +++ b/secretmanager/snippets/list_tag_bindings.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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 +""" +command line application and sample code for listing tag bindings attached to a secret. +""" + +import argparse + +# [START secretmanager_list_tag_bindings] +# Import the Resource Manager client library. +from google.cloud import resourcemanager_v3 +from google.cloud import secretmanager + + +def list_tag_bindings(project_id: str, secret_id: str) -> None: + """ + Lists all tag bindings attached to a secret. + + Args: + project_id (str): The project ID where the secret exists. + secret_id (str): The ID of the secret to list tag bindings for. + + Example: + # List tag bindings for a secret + list_tag_bindings("my-project", "my-secret") + """ + + # Create the Resource Manager client. + client = resourcemanager_v3.TagBindingsClient() + sm_client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the parent secret. + secret_name = sm_client.secret_path(project_id, secret_id) + + parent = f"//secretmanager.googleapis.com/{secret_name}" + + # List all tag bindings. + tag_bindings = [] + request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + + # Retrieve and process tag bindings + bindings = client.list_tag_bindings(request=request) + count = 0 + + print(f"Tag bindings for {secret_name}:") + for binding in bindings: + print(f"- Tag Value: {binding.tag_value}") + tag_bindings.append(binding) + count += 1 + + if count == 0: + print(f"No tag bindings found for {secret_name}.") + + +# [END secretmanager_list_tag_bindings] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument( + "secret_id", help="id of the secret to list tag bindings for" + ) + args = parser.parse_args() + + list_tag_bindings(args.project_id, args.secret_id) diff --git a/secretmanager/snippets/regional_samples/detach_regional_tag.py b/secretmanager/snippets/regional_samples/detach_regional_tag.py new file mode 100644 index 00000000000..e6b47c47ca9 --- /dev/null +++ b/secretmanager/snippets/regional_samples/detach_regional_tag.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_detach_regional_tag_binding] +from google.cloud import resourcemanager_v3 +from google.api_core import client_options + + +def detach_regional_tag( + project_id: str, location_id: str, secret_id: str, tag_value: str +) -> None: + """ + Detaches a tag value from a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + location_id (str): Region where the secret is stored (e.g., "us-central1") + secret_id (str): ID of the secret + tag_value (str): Tag value to detach (e.g., "tagValues/123456789012") + + Example: + # Detach a tag value from a regional secret + detach_regional_tag( + "my-project", + "us-central1", + "my-secret", + "tagValues/123456789012" + ) + """ + # Set up the endpoint for the regional resource manager + rm_endpoint = f"{location_id}-cloudresourcemanager.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=rm_endpoint) + + # Create the Tag Bindings client with the regional endpoint + tag_bindings_client = resourcemanager_v3.TagBindingsClient( + client_options=client_option + ) + + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Format the parent resource for the tag bindings request + parent = f"//secretmanager.googleapis.com/{secret_name}" + + # Find the binding with the specified tag value + binding_name = None + request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + tag_bindings = tag_bindings_client.list_tag_bindings(request=request) + + for binding in tag_bindings: + if binding.tag_value == tag_value: + binding_name = binding.name + break + + if binding_name is None: + print(f"Tag binding for value {tag_value} not found on {secret_name}.") + return + + # Delete the tag binding + request = resourcemanager_v3.DeleteTagBindingRequest(name=binding_name) + operation = tag_bindings_client.delete_tag_binding(request=request) + + # Wait for the operation to complete + operation.result() + + print(f"Detached tag value {tag_value} from {secret_name}") + + +# [END secretmanager_detach_regional_tag_binding] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument( + "location_id", help="id of location where secret is stored" + ) + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "tag_value", help="tag value to detach (e.g., tagValues/123456789012)" + ) + args = parser.parse_args() + + detach_regional_tag( + args.project_id, args.location_id, args.secret_id, args.tag_value + ) diff --git a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py new file mode 100644 index 00000000000..351d39c94f0 --- /dev/null +++ b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_list_regional_secret_tag_bindings] +from google.cloud import resourcemanager_v3 +from google.api_core import client_options + + +def list_regional_secret_tag_bindings( + project_id: str, location_id: str, secret_id: str +) -> None: + """ + Lists tag bindings for a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + location_id (str): Region where the secret should be stored (e.g., "us-central1") + secret_id (str): ID of the secret to create + + Example: + # Create a regional secret with a customer-managed encryption key + list_regional_secret_tag_bindings( + "my-project", + "my-regional-secret-with-cmek", + "us-central1", + ) + """ + # Set up the endpoint for the regional resource manager + rm_endpoint = f"{location_id}-cloudresourcemanager.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=rm_endpoint) + + # Create the Tag Bindings client with the regional endpoint + tag_bindings_client = resourcemanager_v3.TagBindingsClient( + client_options=client_option + ) + + name = f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + + # Format the parent resource for the tag bindings request + parent = f"//secretmanager.googleapis.com/{name}" + + # List the tag bindings + print(f"Tag bindings for {name}:") + count = 0 + + # Use the list_tag_bindings method to get all tag bindings + request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + tag_bindings = tag_bindings_client.list_tag_bindings(request=request) + + # Iterate through the results + for binding in tag_bindings: + print(f"- Tag Value: {binding.tag_value}") + count += 1 + + if count == 0: + print(f"No tag bindings found for {name}.") + + +# [END secretmanager_list_regional_secret_tag_bindings] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument( + "location_id", help="id of location where secret is stored" + ) + parser.add_argument("secret_id", help="id of the secret in which to list") + args = parser.parse_args() + + list_regional_secret_tag_bindings( + args.project_id, args.location_id, args.secret_id + ) diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 436b8d0d11b..671891310fa 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -58,7 +58,8 @@ from regional_samples import update_regional_secret_with_etag from regional_samples import view_regional_secret_annotations from regional_samples import view_regional_secret_labels - +from regional_samples import list_regional_secret_tag_bindings +from regional_samples import detach_regional_tag @pytest.fixture() def location_id() -> str: @@ -858,3 +859,49 @@ def test_view_regional_secret_labels( out, _ = capsys.readouterr() assert label_key in out + +def test_list_regional_secret_tag_bindings(capsys: pytest.LogCaptureFixture, project_id: str, location_id: str, tag_key_and_tag_value: Tuple[str, str], secret_id: str) -> None: + tag_key, tag_value = tag_key_and_tag_value + create_regional_secret_with_tags.create_regional_secret_with_tags( + project_id, location_id, secret_id, tag_key, tag_value + ) + + # Call the function being tested + list_regional_secret_tag_bindings.list_regional_secret_tag_bindings(project_id, location_id, secret_id) + + # Verify the tag value is in the returned bindings + out, _ = capsys.readouterr() + assert secret_id in out + assert tag_value in out + +def test_detach_regional_tag( + capsys: pytest.LogCaptureFixture, + project_id: str, + location_id: str, + tag_key_and_tag_value: Tuple[str, str], + secret_id: str +) -> None: + tag_key, tag_value = tag_key_and_tag_value + + # Create a secret and bind the tag to it for testing detach + create_regional_secret_with_tags.create_regional_secret_with_tags( + project_id, location_id, secret_id, tag_key, tag_value + ) + + # Call the function being tested - detach the tag + detach_regional_tag.detach_regional_tag( + project_id, location_id, secret_id, tag_value + ) + + # Verify the output contains the expected message + out, _ = capsys.readouterr() + assert "Detached tag value" in out + + # List the tags to verify the tag was detached + list_regional_secret_tag_bindings.list_regional_secret_tag_bindings( + project_id, location_id, secret_id + ) + + # Verify the tag value is no longer in the returned bindings + out, _ = capsys.readouterr() + assert tag_value not in out diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index dbcdde921a2..472b8bb5af8 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -63,7 +63,8 @@ from update_secret_with_etag import update_secret_with_etag from view_secret_annotations import view_secret_annotations from view_secret_labels import view_secret_labels - +from list_tag_bindings import list_tag_bindings +from detach_tag_binding import detach_tag @pytest.fixture() def client() -> secretmanager.SecretManagerServiceClient: @@ -745,3 +746,52 @@ def test_update_secret_with_delayed_destroy(secret_with_delayed_destroy: Tuple[s updated_version_destroy_ttl_value = 118400 updated_secret = update_secret_with_delayed_destroy(project_id, secret_id, updated_version_destroy_ttl_value) assert updated_secret.version_destroy_ttl == timedelta(seconds=updated_version_destroy_ttl_value) + +def test_list_tag_bindings( + capsys: pytest.LogCaptureFixture, + project_id: str, + tag_key_and_tag_value: Tuple[str, str], + secret_id: str, +) -> None: + # Get the tag value from the fixture + _, tag_value = tag_key_and_tag_value + + # Create the secret and bind tag (using existing fixtures) + bind_tags_to_secret(project_id, secret_id, tag_value) + + # Call the function being tested + list_tag_bindings(project_id, secret_id) + + # Verify the tag value is in the returned bindings + out, _ = capsys.readouterr() + assert secret_id in out + assert tag_value in out + +def test_detach_tag( + project_id: str, + tag_key_and_tag_value: Tuple[str, str], + secret_id: str, +) -> None: + """Test detaching a tag from a secret.""" + # Get the tag value from the fixture + _, tag_value = tag_key_and_tag_value + + # First bind the tag to the secret + bind_tags_to_secret(project_id, secret_id, tag_value) + secret_name = f"projects/{project_id}/secrets/{secret_id}" + + # Now detach the tag + detach_tag(project_id, secret_id, tag_value) + + client = resourcemanager_v3.TagBindingsClient() + parent = f"//secretmanager.googleapis.com/{secret_name}" + request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + + # Check that none of the bindings contain our tag value + tag_found = False + for binding in client.list_tag_bindings(request=request): + if binding.tag_value == tag_value: + tag_found = True + break + + assert not tag_found, f"Tag value {tag_value} should have been detached but was found" From 7531482b3b93a3ea51362ac22a3b120f0d1ff2d8 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Thu, 15 Jan 2026 20:58:41 +0530 Subject: [PATCH 02/24] feat(secretmanager): Update formatting --- .../regional_samples/detach_regional_tag.py | 2 +- .../list_regional_secret_tag_bindings.py | 2 +- .../regional_samples/snippets_test.py | 48 +++++++++++++------ secretmanager/snippets/snippets_test.py | 34 ++++++++----- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/secretmanager/snippets/regional_samples/detach_regional_tag.py b/secretmanager/snippets/regional_samples/detach_regional_tag.py index e6b47c47ca9..e2b0b344a2b 100644 --- a/secretmanager/snippets/regional_samples/detach_regional_tag.py +++ b/secretmanager/snippets/regional_samples/detach_regional_tag.py @@ -15,8 +15,8 @@ # limitations under the License. # [START secretmanager_detach_regional_tag_binding] -from google.cloud import resourcemanager_v3 from google.api_core import client_options +from google.cloud import resourcemanager_v3 def detach_regional_tag( diff --git a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py index 351d39c94f0..90b330e3f94 100644 --- a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py +++ b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py @@ -15,8 +15,8 @@ # limitations under the License. # [START secretmanager_list_regional_secret_tag_bindings] -from google.cloud import resourcemanager_v3 from google.api_core import client_options +from google.cloud import resourcemanager_v3 def list_regional_secret_tag_bindings( diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 671891310fa..45a1a2f0f6b 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -37,6 +37,7 @@ from regional_samples import delete_regional_secret_with_etag from regional_samples import destroy_regional_secret_version from regional_samples import destroy_regional_secret_version_with_etag +from regional_samples import detach_regional_tag from regional_samples import disable_regional_secret_delayed_destroy from regional_samples import disable_regional_secret_version from regional_samples import disable_regional_secret_version_with_etag @@ -48,6 +49,7 @@ from regional_samples import get_regional_secret_version from regional_samples import iam_grant_access_with_regional_secret from regional_samples import iam_revoke_access_with_regional_secret +from regional_samples import list_regional_secret_tag_bindings from regional_samples import list_regional_secret_versions from regional_samples import list_regional_secret_versions_with_filter from regional_samples import list_regional_secrets @@ -58,8 +60,7 @@ from regional_samples import update_regional_secret_with_etag from regional_samples import view_regional_secret_annotations from regional_samples import view_regional_secret_labels -from regional_samples import list_regional_secret_tag_bindings -from regional_samples import detach_regional_tag + @pytest.fixture() def location_id() -> str: @@ -860,48 +861,65 @@ def test_view_regional_secret_labels( out, _ = capsys.readouterr() assert label_key in out -def test_list_regional_secret_tag_bindings(capsys: pytest.LogCaptureFixture, project_id: str, location_id: str, tag_key_and_tag_value: Tuple[str, str], secret_id: str) -> None: + +def test_list_regional_secret_tag_bindings( + capsys: pytest.LogCaptureFixture, + project_id: str, + location_id: str, + tag_key_and_tag_value: Tuple[str, str], + secret_id: str, +) -> None: tag_key, tag_value = tag_key_and_tag_value create_regional_secret_with_tags.create_regional_secret_with_tags( project_id, location_id, secret_id, tag_key, tag_value ) # Call the function being tested - list_regional_secret_tag_bindings.list_regional_secret_tag_bindings(project_id, location_id, secret_id) - + + list_regional_secret_tag_bindings.list_regional_secret_tag_bindings( + project_id, location_id, secret_id + ) + # Verify the tag value is in the returned bindings + out, _ = capsys.readouterr() assert secret_id in out assert tag_value in out + def test_detach_regional_tag( - capsys: pytest.LogCaptureFixture, - project_id: str, - location_id: str, - tag_key_and_tag_value: Tuple[str, str], - secret_id: str + capsys: pytest.LogCaptureFixture, + project_id: str, + location_id: str, + tag_key_and_tag_value: Tuple[str, str], + secret_id: str, ) -> None: tag_key, tag_value = tag_key_and_tag_value - + # Create a secret and bind the tag to it for testing detach + create_regional_secret_with_tags.create_regional_secret_with_tags( project_id, location_id, secret_id, tag_key, tag_value ) - + # Call the function being tested - detach the tag + detach_regional_tag.detach_regional_tag( project_id, location_id, secret_id, tag_value ) - + # Verify the output contains the expected message + out, _ = capsys.readouterr() assert "Detached tag value" in out - + # List the tags to verify the tag was detached + list_regional_secret_tag_bindings.list_regional_secret_tag_bindings( project_id, location_id, secret_id ) - + # Verify the tag value is no longer in the returned bindings + out, _ = capsys.readouterr() assert tag_value not in out diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index 472b8bb5af8..3e0a35baa0c 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -42,6 +42,7 @@ from delete_secret_with_etag import delete_secret_with_etag from destroy_secret_version import destroy_secret_version from destroy_secret_version_with_etag import destroy_secret_version_with_etag +from detach_tag_binding import detach_tag from disable_secret_version import disable_secret_version from disable_secret_version_with_etag import disable_secret_version_with_etag from disable_secret_with_delayed_destroy import disable_secret_with_delayed_destroy @@ -56,6 +57,7 @@ from list_secret_versions_with_filter import list_secret_versions_with_filter from list_secrets import list_secrets from list_secrets_with_filter import list_secrets_with_filter +from list_tag_bindings import list_tag_bindings from quickstart import quickstart from update_secret import update_secret from update_secret_with_alias import update_secret_with_alias @@ -63,8 +65,7 @@ from update_secret_with_etag import update_secret_with_etag from view_secret_annotations import view_secret_annotations from view_secret_labels import view_secret_labels -from list_tag_bindings import list_tag_bindings -from detach_tag_binding import detach_tag + @pytest.fixture() def client() -> secretmanager.SecretManagerServiceClient: @@ -747,6 +748,7 @@ def test_update_secret_with_delayed_destroy(secret_with_delayed_destroy: Tuple[s updated_secret = update_secret_with_delayed_destroy(project_id, secret_id, updated_version_destroy_ttl_value) assert updated_secret.version_destroy_ttl == timedelta(seconds=updated_version_destroy_ttl_value) + def test_list_tag_bindings( capsys: pytest.LogCaptureFixture, project_id: str, @@ -754,19 +756,24 @@ def test_list_tag_bindings( secret_id: str, ) -> None: # Get the tag value from the fixture + _, tag_value = tag_key_and_tag_value - + # Create the secret and bind tag (using existing fixtures) + bind_tags_to_secret(project_id, secret_id, tag_value) - + # Call the function being tested + list_tag_bindings(project_id, secret_id) - + # Verify the tag value is in the returned bindings + out, _ = capsys.readouterr() assert secret_id in out assert tag_value in out + def test_detach_tag( project_id: str, tag_key_and_tag_value: Tuple[str, str], @@ -774,24 +781,29 @@ def test_detach_tag( ) -> None: """Test detaching a tag from a secret.""" # Get the tag value from the fixture + _, tag_value = tag_key_and_tag_value - + # First bind the tag to the secret + bind_tags_to_secret(project_id, secret_id, tag_value) secret_name = f"projects/{project_id}/secrets/{secret_id}" - + # Now detach the tag + detach_tag(project_id, secret_id, tag_value) - + client = resourcemanager_v3.TagBindingsClient() parent = f"//secretmanager.googleapis.com/{secret_name}" request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) - + # Check that none of the bindings contain our tag value + tag_found = False for binding in client.list_tag_bindings(request=request): if binding.tag_value == tag_value: tag_found = True break - - assert not tag_found, f"Tag value {tag_value} should have been detached but was found" + assert ( + not tag_found + ), f"Tag value {tag_value} should have been detached but was found" From fb569b7e436289bd6980d01b3f994f75145b47f5 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Thu, 15 Jan 2026 22:08:14 +0530 Subject: [PATCH 03/24] feat(secretmanager): Update formatting --- secretmanager/snippets/list_tag_bindings.py | 2 -- .../list_regional_secret_tag_bindings.py | 7 +++---- secretmanager/snippets/snippets_test.py | 21 +++++++------------ 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py index ea04fed9c16..bcd842b20a3 100644 --- a/secretmanager/snippets/list_tag_bindings.py +++ b/secretmanager/snippets/list_tag_bindings.py @@ -47,7 +47,6 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: parent = f"//secretmanager.googleapis.com/{secret_name}" # List all tag bindings. - tag_bindings = [] request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) # Retrieve and process tag bindings @@ -57,7 +56,6 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: print(f"Tag bindings for {secret_name}:") for binding in bindings: print(f"- Tag Value: {binding.tag_value}") - tag_bindings.append(binding) count += 1 if count == 0: diff --git a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py index 90b330e3f94..569206c4f9f 100644 --- a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py +++ b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py @@ -27,15 +27,14 @@ def list_regional_secret_tag_bindings( Args: project_id (str): ID of the Google Cloud project - location_id (str): Region where the secret should be stored (e.g., "us-central1") - secret_id (str): ID of the secret to create + location_id (str): Region where the secret is stored (e.g., "us-central1") + secret_id (str): The ID of the secret to list tag bindings for. Example: - # Create a regional secret with a customer-managed encryption key list_regional_secret_tag_bindings( "my-project", - "my-regional-secret-with-cmek", "us-central1", + "my-regional-secret-with-cmek", ) """ # Set up the endpoint for the regional resource manager diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index 3e0a35baa0c..03f39276715 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -775,6 +775,7 @@ def test_list_tag_bindings( def test_detach_tag( + capsys: pytest.LogCaptureFixture, project_id: str, tag_key_and_tag_value: Tuple[str, str], secret_id: str, @@ -787,23 +788,17 @@ def test_detach_tag( # First bind the tag to the secret bind_tags_to_secret(project_id, secret_id, tag_value) - secret_name = f"projects/{project_id}/secrets/{secret_id}" # Now detach the tag detach_tag(project_id, secret_id, tag_value) - client = resourcemanager_v3.TagBindingsClient() - parent = f"//secretmanager.googleapis.com/{secret_name}" - request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) + out, _ = capsys.readouterr() + assert "Detached tag value" in out + + list_tag_bindings(project_id, secret_id) - # Check that none of the bindings contain our tag value + # Verify the tag value is no longer in the returned bindings - tag_found = False - for binding in client.list_tag_bindings(request=request): - if binding.tag_value == tag_value: - tag_found = True - break - assert ( - not tag_found - ), f"Tag value {tag_value} should have been detached but was found" + out, _ = capsys.readouterr() + assert tag_value not in out From 4645d3139631b5b7aa081635cb82dd1468f398f4 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Thu, 15 Jan 2026 23:03:57 +0530 Subject: [PATCH 04/24] feat(secretmanager): Add expiration samples --- .../snippets/create_secret_with_expiration.py | 78 +++++++++++++++ .../snippets/delete_secret_expiration.py | 68 +++++++++++++ ...create_regional_secret_with_expire_time.py | 91 +++++++++++++++++ .../delete_regional_secret_expiration.py | 93 +++++++++++++++++ .../regional_samples/snippets_test.py | 91 ++++++++++++++++- .../update_regional_secret_expiration.py | 99 +++++++++++++++++++ secretmanager/snippets/snippets_test.py | 77 ++++++++++++++- .../snippets/update_secret_expiration.py | 74 ++++++++++++++ 8 files changed, 669 insertions(+), 2 deletions(-) create mode 100644 secretmanager/snippets/create_secret_with_expiration.py create mode 100644 secretmanager/snippets/delete_secret_expiration.py create mode 100644 secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py create mode 100644 secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py create mode 100644 secretmanager/snippets/regional_samples/update_regional_secret_expiration.py create mode 100644 secretmanager/snippets/update_secret_expiration.py diff --git a/secretmanager/snippets/create_secret_with_expiration.py b/secretmanager/snippets/create_secret_with_expiration.py new file mode 100644 index 00000000000..77f1771edf1 --- /dev/null +++ b/secretmanager/snippets/create_secret_with_expiration.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_secret_with_expiration] +from datetime import datetime, timedelta + +from google.cloud import secretmanager +from google.protobuf import timestamp_pb2 + + +def create_secret_with_expiration(project_id: str, secret_id: str) -> None: + """ + Create a new secret with an expiration time. + + Args: + project_id (str): The ID of the project where the secret will be created. + secret_id (str): The ID for the secret to create. + + Example: + # Create a secret that expires in 24 hours + create_secret_with_expiration("my-project", "my-secret-with-expiry") + """ + expire_time = datetime.now() + timedelta(hours=1) + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the parent project. + parent = f"projects/{project_id}" + + # Convert the Python datetime to a Protobuf Timestamp + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(expire_time) + + # Create the secret with automatic replication and expiration time. + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "replication": { + "automatic": {}, + }, + "expire_time": timestamp, + }, + } + ) + + print(f"Created secret {secret.name} with expiration time {expire_time}") + + +# [END secretmanager_create_secret_with_expiration] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + args = parser.parse_args() + + create_secret_with_expiration(args.project_id, args.secret_id) diff --git a/secretmanager/snippets/delete_secret_expiration.py b/secretmanager/snippets/delete_secret_expiration.py new file mode 100644 index 00000000000..066d2f6fcba --- /dev/null +++ b/secretmanager/snippets/delete_secret_expiration.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_delete_secret_expiration] +from google.cloud import secretmanager +from google.protobuf.field_mask_pb2 import FieldMask + + +def delete_secret_expiration(project_id: str, secret_id: str) -> None: + """ + Removes the expiration time from a secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to update + Example: + # Remove the expiration time from a secret that was previously scheduled for deletion + delete_secret_expiration( + "my-project", + "my-secret-with-expiration" + ) + """ + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret + name = client.secret_path(project_id, secret_id) + + # Create the update mask. + update_mask = FieldMask(paths=["expire_time"]) + + # Build the request. + request = {"secret": {"name": name}, "update_mask": update_mask} + + # Update the secret. + secret = client.update_secret(request=request) + + print(f"Removed expiration from secret {secret.name}") + + +# [END secretmanager_delete_secret_expiration] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to act on") + args = parser.parse_args() + + delete_secret_expiration(args.project_id, args.secret_id) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py new file mode 100644 index 00000000000..409de905a79 --- /dev/null +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_regional_secret_with_expire_time] +from datetime import datetime, timedelta + +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import timestamp_pb2 + + +def create_regional_secret_with_expire_time( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Creates a new regional secret with an expiration time. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + location_id (str): Region where the secret should be stored (e.g., "us-central1") + + Example: + # Create a regional secret that expires in 1 hour + create_regional_secret_with_expire_time("my-project", "my-secret-with-expiry", "us-central1") + """ + # Set expiration time to 1 hour from now + expire_time = datetime.now() + timedelta(hours=1) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Build the resource name of the parent project with location + parent = f"projects/{project_id}/locations/{location_id}" + + # Convert the Python datetime to a Protobuf Timestamp + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(expire_time) + + # Create the secret with expiration time + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": {"expire_time": timestamp}, + } + ) + + print(f"Created secret {secret.name} with expiration time {expire_time}") + + +# [END secretmanager_create_regional_secret_with_expire_time] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "location_id", + help="region where the secret should be stored (e.g., us-central1)", + ) + args = parser.parse_args() + + create_regional_secret_with_expire_time( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py new file mode 100644 index 00000000000..eb3225ff875 --- /dev/null +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +import argparse + +# [START secretmanager_delete_regional_secret_expiration] +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import field_mask_pb2 + + +def delete_regional_secret_expiration( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Removes the expiration time from a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret + location_id (str): Region where the secret is stored (e.g., "us-central1") + + Example: + # Remove expiration from a regional secret + delete_regional_secret_expiration( + "my-project", + "my-secret", + "us-central1" + ) + """ + # Construct the secret name from the component parts + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Create a field mask to update only the expire_time field + update_mask = field_mask_pb2.FieldMask(paths=["expire_time"]) + + # Update the secret to remove the expiration time + secret = client.update_secret( + request={ + "secret": { + "name": secret_name, + }, + "update_mask": update_mask, + } + ) + + print(f"Removed expiration from secret {secret.name}") + + +# [END secretmanager_delete_regional_secret_expiration] + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "location_id", + help="region where the secret is stored (e.g., us-central1)", + ) + args = parser.parse_args() + + delete_regional_secret_expiration( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 45a1a2f0f6b..a3c330719c1 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -from datetime import timedelta +from datetime import datetime, timedelta import os import time from typing import Iterator, Tuple, Union @@ -29,10 +29,12 @@ from regional_samples import create_regional_secret from regional_samples import create_regional_secret_with_annotations from regional_samples import create_regional_secret_with_delayed_destroy +from regional_samples import create_regional_secret_with_expire_time from regional_samples import create_regional_secret_with_labels from regional_samples import create_regional_secret_with_tags from regional_samples import delete_regional_secret from regional_samples import delete_regional_secret_annotation +from regional_samples import delete_regional_secret_expiration from regional_samples import delete_regional_secret_label from regional_samples import delete_regional_secret_with_etag from regional_samples import destroy_regional_secret_version @@ -56,6 +58,7 @@ from regional_samples import list_regional_secrets_with_filter from regional_samples import regional_quickstart from regional_samples import update_regional_secret +from regional_samples import update_regional_secret_expiration from regional_samples import update_regional_secret_with_delayed_destroy from regional_samples import update_regional_secret_with_etag from regional_samples import view_regional_secret_annotations @@ -923,3 +926,89 @@ def test_detach_regional_tag( out, _ = capsys.readouterr() assert tag_value not in out + + +def test_create_regional_secret_with_expire_time( + project_id: str, secret_id: str, location_id: str +) -> None: + # Set expire time to 1 hour from now + + expire_time = datetime.now() + timedelta(hours=1) + create_regional_secret_with_expire_time.create_regional_secret_with_expire_time( + project_id, secret_id, location_id + ) + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + # Verify the secret has an expiration time + + assert ( + retrieved_secret.expire_time is not None + ), "ExpireTime is None, expected non-None" + retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) + retrieved_expire_time = int(retrieved_expire_time.timestamp()) + + # Convert expected datetime to seconds + + expire_time = int(expire_time.timestamp()) + + time_diff = abs(retrieved_expire_time - expire_time) + assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " + + +def test_update_regional_secret_expiration( + capsys: pytest.LogCaptureFixture, project_id: str, secret_id: str, location_id: str +) -> None: + create_regional_secret_with_expire_time.create_regional_secret_with_expire_time( + project_id, secret_id, location_id + ) + + # Update expire time to 2 hours + + new_expire = datetime.now() + timedelta(hours=2) + update_regional_secret_expiration.update_regional_secret_expiration( + project_id, secret_id, location_id + ) + + # Verify output contains expected message + + out, _ = capsys.readouterr() + assert "Updated secret" in out + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + assert ( + retrieved_secret.expire_time is not None + ), "ExpireTime is None, expected non-None" + retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) + retrieved_expire_time = int(retrieved_expire_time.timestamp()) + + new_expire = int(new_expire.timestamp()) + time_diff = abs(retrieved_expire_time - new_expire) + assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " + + +def test_delete_regional_secret_expiration( + capsys: pytest.LogCaptureFixture, project_id: str, secret_id: str, location_id: str +) -> None: + + create_regional_secret_with_expire_time.create_regional_secret_with_expire_time( + project_id, secret_id, location_id + ) + + delete_regional_secret_expiration.delete_regional_secret_expiration( + project_id, secret_id, location_id + ) + out, _ = capsys.readouterr() + assert "Removed expiration" in out + + # Verify expire time is removed with GetSecret + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + assert ( + retrieved_secret.expire_time is None + ), f"ExpireTime is {retrieved_secret.expire_time}, expected None" diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py new file mode 100644 index 00000000000..015a12b80c5 --- /dev/null +++ b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_update_regional_secret_expiration] +from datetime import datetime, timedelta + +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import field_mask_pb2, timestamp_pb2 + + +def update_regional_secret_expiration( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Updates the expiration time of a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret + location_id (str): Region where the secret is stored (e.g., "us-central1") + + Example: + # Update expiration time of a regional secret + update_regional_secret_expiration( + "my-project", + "my-secret", + "us-central1" + ) + """ + # Construct the secret name from the component parts + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Set new expiration time to 2 hours from now + new_expire = datetime.now() + timedelta(hours=2) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Convert the Python datetime to a Protobuf Timestamp + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(new_expire) + + # Create a field mask to update only the expire_time field + update_mask = field_mask_pb2.FieldMask(paths=["expire_time"]) + + # Update the secret with the new expiration time + secret = client.update_secret( + request={ + "secret": {"name": secret_name, "expire_time": timestamp}, + "update_mask": update_mask, + } + ) + + print(f"Updated secret {secret.name} expiration time to {new_expire}") + + +# [END secretmanager_update_regional_secret_expiration] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "location_id", + help="region where the secret is stored (e.g., us-central1)", + ) + args = parser.parse_args() + + update_regional_secret_expiration( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index 03f39276715..48618897ad2 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and import base64 -from datetime import timedelta +from datetime import datetime, timedelta import os import time from typing import Iterator, Optional, Tuple, Union @@ -32,12 +32,14 @@ from create_secret import create_secret from create_secret_with_annotations import create_secret_with_annotations from create_secret_with_delayed_destroy import create_secret_with_delayed_destroy +from create_secret_with_expiration import create_secret_with_expiration from create_secret_with_labels import create_secret_with_labels from create_secret_with_tags import create_secret_with_tags from create_secret_with_user_managed_replication import create_ummr_secret from create_update_secret_label import create_update_secret_label from delete_secret import delete_secret from delete_secret_annotation import delete_secret_annotation +from delete_secret_expiration import delete_secret_expiration from delete_secret_label import delete_secret_label from delete_secret_with_etag import delete_secret_with_etag from destroy_secret_version import destroy_secret_version @@ -60,6 +62,7 @@ from list_tag_bindings import list_tag_bindings from quickstart import quickstart from update_secret import update_secret +from update_secret_expiration import update_secret_expiration from update_secret_with_alias import update_secret_with_alias from update_secret_with_delayed_destroy import update_secret_with_delayed_destroy from update_secret_with_etag import update_secret_with_etag @@ -802,3 +805,75 @@ def test_detach_tag( out, _ = capsys.readouterr() assert tag_value not in out + + +def test_create_secret_with_expiration(project_id: str, secret_id: str) -> None: + """Test creating a secret with an expiration time.""" + + # Set expire time to 1 hour from now + + expire_time = datetime.now() + timedelta(hours=1) + create_secret_with_expiration(project_id, secret_id) + + retrieved_secret = get_secret(project_id, secret_id) + # Verify the secret has an expiration time + + assert ( + retrieved_secret.expire_time is not None + ), "ExpireTime is None, expected non-None" + retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) + retrieved_expire_time = int(retrieved_expire_time.timestamp()) + + # Convert expected datetime to seconds + + expire_time = int(expire_time.timestamp()) + + time_diff = abs(retrieved_expire_time - expire_time) + assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " + + +def test_update_secret_expiration( + capsys: pytest.LogCaptureFixture, + project_id: str, + secret_id: str, +) -> None: + create_secret_with_expiration(project_id, secret_id) + + # Update expire time to 2 hours + + new_expire = datetime.now() + timedelta(hours=2) # 2 hours from now in seconds + update_secret_expiration(project_id, secret_id) + + # Verify output contains expected message + + out, _ = capsys.readouterr() + assert "Updated secret" in out + + retrieved_secret = get_secret(project_id, secret_id) + assert ( + retrieved_secret.expire_time is not None + ), "ExpireTime is None, expected non-None" + retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) + retrieved_expire_time = int(retrieved_expire_time.timestamp()) + + new_expire = int(new_expire.timestamp()) + time_diff = abs(retrieved_expire_time - new_expire) + assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " + + +def test_delete_expiration( + capsys: pytest.LogCaptureFixture, project_id: str, secret_id: str +) -> None: + + create_secret_with_expiration(project_id, secret_id) + + delete_secret_expiration(project_id, secret_id) + out, _ = capsys.readouterr() + assert "Removed expiration" in out + + # Verify expire time is removed with GetSecret + + retrieved_secret = get_secret(project_id, secret_id) + assert ( + retrieved_secret.expire_time is None + ), f"ExpireTime is {retrieved_secret.expire_time}, expected None" diff --git a/secretmanager/snippets/update_secret_expiration.py b/secretmanager/snippets/update_secret_expiration.py new file mode 100644 index 00000000000..9e98061cdaf --- /dev/null +++ b/secretmanager/snippets/update_secret_expiration.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +import argparse + +# [START secretmanager_update_secret_expiration] +from datetime import datetime, timedelta + +from google.cloud import secretmanager +from google.protobuf import timestamp_pb2 + + +def update_secret_expiration(project_id: str, secret_id: str) -> None: + """ + Update the expiration time of an existing secret. + + Args: + project_id: ID of the Google Cloud project. + secret_id: ID of the secret to update. + + Example: + # Update the expiration time of a secret to 48 hours from now + update_secret_expiration( + "my-project", + "my-secret-with-expiration" + ) + """ + new_expire_time = datetime.now() + timedelta(hours=2) + + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret. + name = client.secret_path(project_id, secret_id) + + # Update the expire_time. + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(new_expire_time) + secret = {"name": name, "expire_time": timestamp} + update_mask = {"paths": ["expire_time"]} + response = client.update_secret( + request={"secret": secret, "update_mask": update_mask} + ) + + # Print the updated secret name. + print(f"Updated secret {response.name} expiration time to {new_expire_time}") + + +# [END secretmanager_update_secret_expiration] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="ID of the GCP project") + parser.add_argument("secret_id", help="ID of the secret to update") + args = parser.parse_args() + + update_secret_expiration(args.project_id, args.secret_id) From d415d0794b182631c254484ce8935bba48f73951 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Thu, 15 Jan 2026 23:07:36 +0530 Subject: [PATCH 05/24] feat(secretmanager): Update formatting --- secretmanager/snippets/detach_tag_binding.py | 4 +--- secretmanager/snippets/list_tag_bindings.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/secretmanager/snippets/detach_tag_binding.py b/secretmanager/snippets/detach_tag_binding.py index 4afa9153566..2a3cd07629c 100644 --- a/secretmanager/snippets/detach_tag_binding.py +++ b/secretmanager/snippets/detach_tag_binding.py @@ -73,9 +73,7 @@ def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument( - "secret_id", help="id of the secret to detach tag from" - ) + parser.add_argument("secret_id", help="id of the secret to detach tag from") parser.add_argument( "tag_value", help="tag value to detach (e.g., 'tagValues/123456789012')", diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py index bcd842b20a3..7d586e8dc1b 100644 --- a/secretmanager/snippets/list_tag_bindings.py +++ b/secretmanager/snippets/list_tag_bindings.py @@ -71,9 +71,7 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument( - "secret_id", help="id of the secret to list tag bindings for" - ) + parser.add_argument("secret_id", help="id of the secret to list tag bindings for") args = parser.parse_args() list_tag_bindings(args.project_id, args.secret_id) From 8c78e1a610d615383295175199c990918dc4c66a Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 00:07:57 +0530 Subject: [PATCH 06/24] feat(secretmanager): Update formatting --- .../snippets/create_secret_with_expiration.py | 6 ++--- ...create_regional_secret_with_expire_time.py | 4 ++-- .../regional_samples/snippets_test.py | 18 +++++++-------- .../update_regional_secret_expiration.py | 4 ++-- secretmanager/snippets/snippets_test.py | 23 +++++++++---------- .../snippets/update_secret_expiration.py | 9 ++++---- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/secretmanager/snippets/create_secret_with_expiration.py b/secretmanager/snippets/create_secret_with_expiration.py index 77f1771edf1..9af6d04dd6d 100644 --- a/secretmanager/snippets/create_secret_with_expiration.py +++ b/secretmanager/snippets/create_secret_with_expiration.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_secret_with_expiration] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.cloud import secretmanager from google.protobuf import timestamp_pb2 @@ -30,10 +30,10 @@ def create_secret_with_expiration(project_id: str, secret_id: str) -> None: secret_id (str): The ID for the secret to create. Example: - # Create a secret that expires in 24 hours + # Create a secret that expires in 1 hour create_secret_with_expiration("my-project", "my-secret-with-expiry") """ - expire_time = datetime.now() + timedelta(hours=1) + expire_time = datetime.now(timezone.utc) + timedelta(hours=1) # Create the Secret Manager client. client = secretmanager.SecretManagerServiceClient() diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py index 409de905a79..9d35fe1f765 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_regional_secret_with_expire_time] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.api_core import client_options from google.cloud import secretmanager @@ -38,7 +38,7 @@ def create_regional_secret_with_expire_time( create_regional_secret_with_expire_time("my-project", "my-secret-with-expiry", "us-central1") """ # Set expiration time to 1 hour from now - expire_time = datetime.now() + timedelta(hours=1) + expire_time = datetime.now(timezone.utc) + timedelta(hours=1) # Set up the endpoint for the specific region endpoint = f"secretmanager.{location_id}.rep.googleapis.com" diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index a3c330719c1..69ce2212eec 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import os import time from typing import Iterator, Tuple, Union @@ -933,7 +933,7 @@ def test_create_regional_secret_with_expire_time( ) -> None: # Set expire time to 1 hour from now - expire_time = datetime.now() + timedelta(hours=1) + expire_time = datetime.now(timezone.utc) + timedelta(hours=1) create_regional_secret_with_expire_time.create_regional_secret_with_expire_time( project_id, secret_id, location_id ) @@ -946,14 +946,14 @@ def test_create_regional_secret_with_expire_time( assert ( retrieved_secret.expire_time is not None ), "ExpireTime is None, expected non-None" - retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) - retrieved_expire_time = int(retrieved_expire_time.timestamp()) + retrieved_expire_time = retrieved_secret.expire_time.astimezone(timezone.utc) + retrieved_timestamp = int(retrieved_expire_time.timestamp()) # Convert expected datetime to seconds expire_time = int(expire_time.timestamp()) - time_diff = abs(retrieved_expire_time - expire_time) + time_diff = abs(retrieved_timestamp - expire_time) assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " @@ -966,7 +966,7 @@ def test_update_regional_secret_expiration( # Update expire time to 2 hours - new_expire = datetime.now() + timedelta(hours=2) + new_expire = datetime.now(timezone.utc) + timedelta(hours=2) update_regional_secret_expiration.update_regional_secret_expiration( project_id, secret_id, location_id ) @@ -982,11 +982,11 @@ def test_update_regional_secret_expiration( assert ( retrieved_secret.expire_time is not None ), "ExpireTime is None, expected non-None" - retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) - retrieved_expire_time = int(retrieved_expire_time.timestamp()) + retrieved_expire_time = retrieved_secret.expire_time.astimezone(timezone.utc) + retrieved_timestamp = int(retrieved_expire_time.timestamp()) new_expire = int(new_expire.timestamp()) - time_diff = abs(retrieved_expire_time - new_expire) + time_diff = abs(retrieved_timestamp - new_expire) assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py index 015a12b80c5..74f83566127 100644 --- a/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py +++ b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_update_regional_secret_expiration] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.api_core import client_options from google.cloud import secretmanager @@ -47,7 +47,7 @@ def update_regional_secret_expiration( ) # Set new expiration time to 2 hours from now - new_expire = datetime.now() + timedelta(hours=2) + new_expire = datetime.now(timezone.utc) + timedelta(hours=2) # Set up the endpoint for the specific region endpoint = f"secretmanager.{location_id}.rep.googleapis.com" diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index 48618897ad2..e909cd97329 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and import base64 -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import os import time from typing import Iterator, Optional, Tuple, Union @@ -812,7 +812,7 @@ def test_create_secret_with_expiration(project_id: str, secret_id: str) -> None: # Set expire time to 1 hour from now - expire_time = datetime.now() + timedelta(hours=1) + expire_time = datetime.now(timezone.utc) + timedelta(hours=1) create_secret_with_expiration(project_id, secret_id) retrieved_secret = get_secret(project_id, secret_id) @@ -821,15 +821,14 @@ def test_create_secret_with_expiration(project_id: str, secret_id: str) -> None: assert ( retrieved_secret.expire_time is not None ), "ExpireTime is None, expected non-None" - retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) - retrieved_expire_time = int(retrieved_expire_time.timestamp()) - # Convert expected datetime to seconds + retrieved_expire_time = retrieved_secret.expire_time.astimezone(timezone.utc) - expire_time = int(expire_time.timestamp()) + retrieved_timestamp = int(retrieved_expire_time.timestamp()) + expected_timestamp = int(expire_time.timestamp()) - time_diff = abs(retrieved_expire_time - expire_time) - assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " + time_diff = abs(retrieved_timestamp - expected_timestamp) + assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds." def test_update_secret_expiration( @@ -841,7 +840,7 @@ def test_update_secret_expiration( # Update expire time to 2 hours - new_expire = datetime.now() + timedelta(hours=2) # 2 hours from now in seconds + new_expire = datetime.now(timezone.utc) + timedelta(hours=2) # 2 hours from now in seconds update_secret_expiration(project_id, secret_id) # Verify output contains expected message @@ -853,11 +852,11 @@ def test_update_secret_expiration( assert ( retrieved_secret.expire_time is not None ), "ExpireTime is None, expected non-None" - retrieved_expire_time = retrieved_secret.expire_time.replace(tzinfo=None) - retrieved_expire_time = int(retrieved_expire_time.timestamp()) + retrieved_expire_time = retrieved_secret.expire_time.astimezone(timezone.utc) + retrieved_timestamp = int(retrieved_expire_time.timestamp()) new_expire = int(new_expire.timestamp()) - time_diff = abs(retrieved_expire_time - new_expire) + time_diff = abs(retrieved_timestamp - new_expire) assert time_diff <= 1, f"ExpireTime difference too large: {time_diff} seconds. " diff --git a/secretmanager/snippets/update_secret_expiration.py b/secretmanager/snippets/update_secret_expiration.py index 9e98061cdaf..cd460cb82e6 100644 --- a/secretmanager/snippets/update_secret_expiration.py +++ b/secretmanager/snippets/update_secret_expiration.py @@ -14,10 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse # [START secretmanager_update_secret_expiration] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.cloud import secretmanager from google.protobuf import timestamp_pb2 @@ -32,13 +31,13 @@ def update_secret_expiration(project_id: str, secret_id: str) -> None: secret_id: ID of the secret to update. Example: - # Update the expiration time of a secret to 48 hours from now + # Update the expiration time of a secret to 2 hours from now update_secret_expiration( "my-project", "my-secret-with-expiration" ) """ - new_expire_time = datetime.now() + timedelta(hours=2) + new_expire_time = datetime.now(timezone.utc) + timedelta(hours=2) # Create the Secret Manager client. client = secretmanager.SecretManagerServiceClient() @@ -63,6 +62,8 @@ def update_secret_expiration(project_id: str, secret_id: str) -> None: if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, From 989d4ed32dff9a2ed4a632903b54f93b4c911d2f Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 00:12:21 +0530 Subject: [PATCH 07/24] feat(secretmanager): Update formatting --- .../regional_samples/delete_regional_secret_expiration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py index eb3225ff875..c69a6a0c8a4 100644 --- a/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse # [START secretmanager_delete_regional_secret_expiration] from google.api_core import client_options @@ -75,6 +74,7 @@ def delete_regional_secret_expiration( if __name__ == "__main__": + import argparse parser = argparse.ArgumentParser( description=__doc__, From a79c77c85ed7304ee5d378897dd609443417cd9d Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 00:19:03 +0530 Subject: [PATCH 08/24] feat(secretmanager): Update formatting --- secretmanager/snippets/detach_tag_binding.py | 2 +- secretmanager/snippets/list_tag_bindings.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/secretmanager/snippets/detach_tag_binding.py b/secretmanager/snippets/detach_tag_binding.py index 2a3cd07629c..438545361f2 100644 --- a/secretmanager/snippets/detach_tag_binding.py +++ b/secretmanager/snippets/detach_tag_binding.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse # [START secretmanager_detach_tag_binding] from google.cloud import resourcemanager_v3 @@ -67,6 +66,7 @@ def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: if __name__ == "__main__": + import argparse parser = argparse.ArgumentParser( description=__doc__, diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py index 7d586e8dc1b..887b87e8d84 100644 --- a/secretmanager/snippets/list_tag_bindings.py +++ b/secretmanager/snippets/list_tag_bindings.py @@ -16,7 +16,6 @@ command line application and sample code for listing tag bindings attached to a secret. """ -import argparse # [START secretmanager_list_tag_bindings] # Import the Resource Manager client library. @@ -66,6 +65,8 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, From 0a36786420fd0daac6dc6b6d3acb75f44f34c274 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 10:24:23 +0530 Subject: [PATCH 09/24] feat(secretmanager): Adding secret rotation samples --- .../snippets/create_secret_with_rotation.py | 101 +++++++++++ .../snippets/create_secret_with_topic.py | 76 +++++++++ .../snippets/delete_secret_rotation.py | 68 ++++++++ .../create_regional_secret_with_rotation.py | 117 +++++++++++++ .../create_regional_secret_with_topic.py | 92 ++++++++++ .../delete_regional_secret_rotation.py | 92 ++++++++++ .../regional_samples/snippets_test.py | 158 ++++++++++++++++++ .../update_regional_secret_rotation.py | 108 ++++++++++++ secretmanager/snippets/snippets_test.py | 125 +++++++++++++- .../snippets/update_secret_rotation.py | 88 ++++++++++ testing/test-env.tmpl.sh | 1 + 11 files changed, 1025 insertions(+), 1 deletion(-) create mode 100644 secretmanager/snippets/create_secret_with_rotation.py create mode 100644 secretmanager/snippets/create_secret_with_topic.py create mode 100644 secretmanager/snippets/delete_secret_rotation.py create mode 100644 secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py create mode 100644 secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py create mode 100644 secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py create mode 100644 secretmanager/snippets/regional_samples/update_regional_secret_rotation.py create mode 100644 secretmanager/snippets/update_secret_rotation.py diff --git a/secretmanager/snippets/create_secret_with_rotation.py b/secretmanager/snippets/create_secret_with_rotation.py new file mode 100644 index 00000000000..70a3dfe5a81 --- /dev/null +++ b/secretmanager/snippets/create_secret_with_rotation.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_secret_with_rotation] +from datetime import datetime, timedelta + +from google.cloud import secretmanager +from google.protobuf import duration_pb2 +from google.protobuf import timestamp_pb2 + + +def create_secret_with_rotation( + project_id: str, secret_id: str, topic_name: str +) -> None: + """ + Creates a new secret with rotation configured. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + topic_name (str): Resource name of the Pub/Sub topic for rotation notifications + Example: + # Create a secret with automatic rotation every 30 days + create_secret_with_rotation( + "my-project", + "my-rotating-secret", + "projects/my-project/topics/my-rotation-topic" + ) + """ + rotation_period_hours = 24 + # Create the Secret Manager client + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the parent project + parent = f"projects/{project_id}" + + # Convert rotation period to protobuf Duration + rotation_period = duration_pb2.Duration() + rotation_period.seconds = rotation_period_hours * 3600 # Convert hours to seconds + + # Set next rotation time to 24 hours from now + next_rotation_time = timestamp_pb2.Timestamp() + next_rotation_time.FromDatetime(datetime.now() + timedelta(hours=24)) + + # Create the secret with rotation configuration + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "replication": {"automatic": {}}, + "topics": [{"name": topic_name}], + "rotation": { + "next_rotation_time": next_rotation_time, + "rotation_period": rotation_period, + }, + }, + } + ) + + print( + f"Created secret {secret.name} with rotation period {rotation_period_hours} hours and topic {topic_name}" + ) + + +# [END secretmanager_create_secret_with_rotation] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="ID of the GCP project") + parser.add_argument("secret_id", help="ID of the secret to create") + parser.add_argument( + "topic_name", + help="Resource name of the Pub/Sub topic for rotation notifications", + ) + args = parser.parse_args() + + create_secret_with_rotation( + args.project_id, + args.secret_id, + args.topic_name, + ) diff --git a/secretmanager/snippets/create_secret_with_topic.py b/secretmanager/snippets/create_secret_with_topic.py new file mode 100644 index 00000000000..b161c167fb0 --- /dev/null +++ b/secretmanager/snippets/create_secret_with_topic.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_secret_with_topic] +from google.cloud import secretmanager + + +def create_secret_with_topic(project_id: str, secret_id: str, topic_name: str) -> None: + """ + Creates a new secret with a notification topic configured. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + topic_name (str): Full name of the topic in the format "projects/my-project/topics/my-topic" + Example: + # Create a secret with a Pub/Sub notification configuration + create_secret_with_topic( + "my-project", + "my-secret-with-notifications", + "projects/my-project/topics/my-secret-topic" + ) + """ + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the parent name. + parent = f"projects/{project_id}" + + # Create the secret with topic configuration. + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "replication": {"automatic": {}}, + "topics": [{"name": topic_name}], + }, + } + ) + + print(f"Created secret {secret.name} with topic {topic_name}") + + +# [END secretmanager_create_secret_with_topic] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "topic_name", + help="name of the topic in the format 'projects/my-project/topics/my-topic'", + ) + args = parser.parse_args() + + create_secret_with_topic(args.project_id, args.secret_id, args.topic_name) diff --git a/secretmanager/snippets/delete_secret_rotation.py b/secretmanager/snippets/delete_secret_rotation.py new file mode 100644 index 00000000000..d94770e833b --- /dev/null +++ b/secretmanager/snippets/delete_secret_rotation.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_delete_secret_rotation] +from google.cloud import secretmanager +from google.protobuf.field_mask_pb2 import FieldMask + + +def delete_secret_rotation(project_id: str, secret_id: str) -> None: + """ + Removes the rotation configuration from a secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret with rotation to remove + Example: + # Remove the rotation configuration from a secret + delete_secret_rotation( + "my-project", + "my-secret-with-rotation" + ) + """ + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret + name = client.secret_path(project_id, secret_id) + + # Create the update mask. + update_mask = FieldMask(paths=["rotation"]) + + # Build the request. + request = {"secret": {"name": name}, "update_mask": update_mask} + + # Update the secret. + secret = client.update_secret(request=request) + + print(f"Removed rotation from secret {secret.name}") + + +# [END secretmanager_delete_secret_rotation] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to act on") + args = parser.parse_args() + + delete_secret_rotation(args.project_id, args.secret_id) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py new file mode 100644 index 00000000000..f9a88a97007 --- /dev/null +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_regional_secret_with_rotation] +from datetime import datetime, timedelta + +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import duration_pb2, timestamp_pb2 + + +def create_regional_secret_with_rotation( + project_id: str, secret_id: str, location_id: str, topic_name: str +) -> None: + """ + Creates a new regional secret with rotation configured. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + location_id (str): Region where the secret should be stored (e.g., "us-central1") + topic_name (str): Full resource name of the Pub/Sub topic for notifications + (e.g., "projects/my-project/topics/my-topic") + + Example: + # Create a regional secret with rotation + create_regional_secret_with_rotation( + "my-project", + "my-secret-with-rotation", + "us-central1", + "projects/my-project/topics/my-topic" + ) + """ + # Set rotation period to 24 hours + rotation_period = timedelta(hours=24) + + # Set next rotation time to 24 hours from now + next_rotation_time = datetime.now() + timedelta(hours=24) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Build the resource name of the parent project with location + parent = f"projects/{project_id}/locations/{location_id}" + + # Convert the Python datetime to a Protobuf Timestamp + next_rotation_timestamp = timestamp_pb2.Timestamp() + next_rotation_timestamp.FromDatetime(next_rotation_time) + + # Convert the Python timedelta to a Protobuf Duration + rotation_period_proto = duration_pb2.Duration() + rotation_period_proto.FromTimedelta(rotation_period) + + # Create the secret with rotation configuration and topic + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "topics": [{"name": topic_name}], + "rotation": { + "next_rotation_time": next_rotation_timestamp, + "rotation_period": rotation_period_proto, + }, + }, + } + ) + + print( + f"Created secret {secret.name} with rotation period {rotation_period} and topic {topic_name}" + ) + + +# [END secretmanager_create_regional_secret_with_rotation] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "location_id", + help="region where the secret should be stored (e.g., us-central1)", + ) + parser.add_argument( + "topic_name", + help="full resource name of the Pub/Sub topic for notifications", + ) + args = parser.parse_args() + + create_regional_secret_with_rotation( + args.project_id, args.secret_id, args.location_id, args.topic_name + ) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py new file mode 100644 index 00000000000..a52ddf1daa1 --- /dev/null +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_regional_secret_with_topic] +from google.api_core import client_options +from google.cloud import secretmanager + + +def create_regional_secret_with_topic( + project_id: str, secret_id: str, location_id: str, topic_name: str +) -> None: + """ + Creates a new regional secret with a Pub/Sub topic configured for notifications. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + location_id (str): Region where the secret should be stored (e.g., "us-central1") + topic_name (str): Full resource name of the Pub/Sub topic for notifications + (e.g., "projects/my-project/topics/my-topic") + + Example: + # Create a regional secret with a Pub/Sub topic + create_regional_secret_with_topic( + "my-project", + "my-secret-with-topic", + "us-central1", + "projects/my-project/topics/my-topic" + ) + """ + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Build the resource name of the parent project with location + parent = f"projects/{project_id}/locations/{location_id}" + + # Create the secret with a topic for notifications + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": {"topics": [{"name": topic_name}]}, + } + ) + + print(f"Created secret {secret.name} with topic {topic_name}") + + +# [END secretmanager_create_regional_secret_with_topic] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "location_id", + help="region where the secret should be stored (e.g., us-central1)", + ) + parser.add_argument( + "topic_name", + help="full resource name of the Pub/Sub topic for notifications", + ) + args = parser.parse_args() + + create_regional_secret_with_topic( + args.project_id, args.secret_id, args.location_id, args.topic_name + ) diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py new file mode 100644 index 00000000000..6ec767b2ef4 --- /dev/null +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +import argparse + +# [START secretmanager_delete_regional_secret_rotation] +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import field_mask_pb2 + + +def delete_regional_secret_rotation( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Removes the rotation configuration from a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret + location_id (str): Region where the secret is stored (e.g., "us-central1") + + Example: + # Delete rotation configuration from a regional secret + delete_regional_secret_rotation( + "my-project", + "my-secret-with-rotation", + "us-central1" + ) + """ + # Construct the secret name from the component parts + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Create a field mask to update only the rotation field + update_mask = field_mask_pb2.FieldMask(paths=["rotation"]) + + # Update the secret to remove the rotation configuration + result = client.update_secret( + request={ + "secret": { + "name": secret_name, + }, + "update_mask": update_mask, + } + ) + + print(f"Removed rotation from secret {result.name}") + + +# [END secretmanager_delete_regional_secret_rotation] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "location_id", + help="region where the secret is stored (e.g., us-central1)", + ) + args = parser.parse_args() + + delete_regional_secret_rotation( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 69ce2212eec..04f0e534246 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -31,11 +31,14 @@ from regional_samples import create_regional_secret_with_delayed_destroy from regional_samples import create_regional_secret_with_expire_time from regional_samples import create_regional_secret_with_labels +from regional_samples import create_regional_secret_with_rotation from regional_samples import create_regional_secret_with_tags +from regional_samples import create_regional_secret_with_topic from regional_samples import delete_regional_secret from regional_samples import delete_regional_secret_annotation from regional_samples import delete_regional_secret_expiration from regional_samples import delete_regional_secret_label +from regional_samples import delete_regional_secret_rotation from regional_samples import delete_regional_secret_with_etag from regional_samples import destroy_regional_secret_version from regional_samples import destroy_regional_secret_version_with_etag @@ -59,6 +62,7 @@ from regional_samples import regional_quickstart from regional_samples import update_regional_secret from regional_samples import update_regional_secret_expiration +from regional_samples import update_regional_secret_rotation from regional_samples import update_regional_secret_with_delayed_destroy from regional_samples import update_regional_secret_with_etag from regional_samples import view_regional_secret_annotations @@ -108,6 +112,16 @@ def iam_user() -> str: return "serviceAccount:" + os.environ["GCLOUD_SECRETS_SERVICE_ACCOUNT"] +@pytest.fixture() +def topic_name() -> str: + return os.environ["GOOGLE_CLOUD_TOPIC_NAME"] + + +@pytest.fixture() +def rotation_period_hours() -> int: + return 24 + + @pytest.fixture() def ttl() -> str: return "300s" @@ -1012,3 +1026,147 @@ def test_delete_regional_secret_expiration( assert ( retrieved_secret.expire_time is None ), f"ExpireTime is {retrieved_secret.expire_time}, expected None" + + +def test_create_regional_secret_with_rotation( + capsys: pytest.LogCaptureFixture, + project_id: str, + secret_id: str, + location_id: str, + topic_name: str, + rotation_period_hours: int, +) -> None: + + # Create the secret with rotation + + create_regional_secret_with_rotation.create_regional_secret_with_rotation( + project_id, secret_id, location_id, topic_name + ) + + # Verify output contains expected message + + out, _ = capsys.readouterr() + assert "Created secret" in out, f"Expected 'Created secret' in output, got: {out}" + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + + # Verify rotation is configured + + assert retrieved_secret.rotation is not None, "Rotation is None, expected non-None" + + # Verify rotation period is set correctly (24 hours = 86400 seconds) + + expected_seconds = rotation_period_hours * 3600 + actual_seconds = retrieved_secret.rotation.rotation_period.total_seconds() + assert ( + actual_seconds == expected_seconds + ), f"RotationPeriod mismatch: got {actual_seconds}, want {expected_seconds}" + + # Verify next rotation time is set + + assert ( + retrieved_secret.rotation.next_rotation_time is not None + ), "NextRotationTime is None, expected non-None" + + +def test_update_regional_secret_rotation_period( + capsys: pytest.LogCaptureFixture, + project_id: str, + secret_id: str, + location_id: str, + topic_name: str, +) -> None: + + create_regional_secret_with_rotation.create_regional_secret_with_rotation( + project_id, secret_id, location_id, topic_name + ) + capsys.readouterr() + + updated_rotation_hours = 48 + update_regional_secret_rotation.update_regional_secret_rotation_period( + project_id, secret_id, location_id + ) + + # Verify output contains the secret ID + + out, _ = capsys.readouterr() + assert secret_id in out, f"Expected '{secret_id}' in output, got: {out}" + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + assert ( + retrieved_secret.rotation is not None + ), "GetSecret: Rotation is nil, expected non-nil" + expected_seconds = updated_rotation_hours * 3600 + actual_seconds = retrieved_secret.rotation.rotation_period.total_seconds() + assert ( + actual_seconds == expected_seconds + ), f"RotationPeriod mismatch: got {actual_seconds}, want {expected_seconds}" + + +def test_delete_regional_secret_rotation( + capsys: pytest.LogCaptureFixture, + project_id: str, + secret_id: str, + location_id: str, + topic_name: str, +) -> None: + # First create a secret with rotation configuration + + create_regional_secret_with_rotation.create_regional_secret_with_rotation( + project_id, secret_id, location_id, topic_name + ) + + # Call the function to delete the rotation configuration + + delete_regional_secret_rotation.delete_regional_secret_rotation( + project_id, secret_id, location_id + ) + + # Check the output contains the expected message + + out, _ = capsys.readouterr() + assert "Removed rotation" in out + assert secret_id in out + + # Verify rotation is removed with GetSecret + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + + # Check that rotation configuration is removed + + assert ( + not retrieved_secret.rotation + ), f"Rotation is {repr(retrieved_secret.rotation)}, expected None or empty" + + +def test_create_regional_secret_with_topic( + capsys, project_id: str, secret_id: str, location_id: str, topic_name: str +): + + # Call the function being tested + + create_regional_secret_with_topic.create_regional_secret_with_topic( + project_id, secret_id, location_id, topic_name + ) + + # Check the output contains expected text + + out, _ = capsys.readouterr() + assert "Created secret" in out + + retrived_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + + assert ( + len(retrived_secret.topics) == 1 + ), f"Expected 1 topic, got {len(retrived_secret.topics)}" + assert ( + retrived_secret.topics[0].name == topic_name + ), f"Topic mismatch: got {retrived_secret.topics[0].name}, want {topic_name}" diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py new file mode 100644 index 00000000000..56ab0df8ec9 --- /dev/null +++ b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +import argparse + +# [START secretmanager_update_regional_secret_rotation_period] +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import duration_pb2, field_mask_pb2 + + +def update_regional_secret_rotation_period( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Updates the rotation period of a regional secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret + location_id (str): Region where the secret is stored (e.g., "us-central1") + + Example: + # Update rotation period of a regional secret + update_regional_secret_rotation_period( + "my-project", + "my-secret", + "us-central1" + ) + """ + # Construct the secret name from the component parts + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Set updated rotation period to 48 hours + new_rotation_period_hours = 48 + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Convert the Python timedelta to a Protobuf Duration + rotation_period = duration_pb2.Duration() + rotation_period.seconds = ( + new_rotation_period_hours * 3600 + ) # Convert hours to seconds + + # Create a field mask to update only the rotation_period field + update_mask = field_mask_pb2.FieldMask(paths=["rotation.rotation_period"]) + + # Update the secret with the new rotation period + result = client.update_secret( + request={ + "secret": { + "name": secret_name, + "rotation": {"rotation_period": rotation_period}, + }, + "update_mask": update_mask, + } + ) + + # Get the rotation period in hours for display + rotation_hours = result.rotation.rotation_period.seconds / 3600 + + print( + f"Updated secret {result.name} rotation period to {rotation_hours} hours" + ) + + +# [END secretmanager_update_regional_secret_rotation_period] + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "location_id", + help="region where the secret is stored (e.g., us-central1)", + ) + args = parser.parse_args() + + update_regional_secret_rotation_period( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index e909cd97329..fea045f4938 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -34,13 +34,16 @@ from create_secret_with_delayed_destroy import create_secret_with_delayed_destroy from create_secret_with_expiration import create_secret_with_expiration from create_secret_with_labels import create_secret_with_labels +from create_secret_with_rotation import create_secret_with_rotation from create_secret_with_tags import create_secret_with_tags +from create_secret_with_topic import create_secret_with_topic from create_secret_with_user_managed_replication import create_ummr_secret from create_update_secret_label import create_update_secret_label from delete_secret import delete_secret from delete_secret_annotation import delete_secret_annotation from delete_secret_expiration import delete_secret_expiration from delete_secret_label import delete_secret_label +from delete_secret_rotation import delete_secret_rotation from delete_secret_with_etag import delete_secret_with_etag from destroy_secret_version import destroy_secret_version from destroy_secret_version_with_etag import destroy_secret_version_with_etag @@ -63,6 +66,7 @@ from quickstart import quickstart from update_secret import update_secret from update_secret_expiration import update_secret_expiration +from update_secret_rotation import update_secret_rotation from update_secret_with_alias import update_secret_with_alias from update_secret_with_delayed_destroy import update_secret_with_delayed_destroy from update_secret_with_etag import update_secret_with_etag @@ -95,6 +99,16 @@ def iam_user() -> str: return "serviceAccount:" + os.environ["GCLOUD_SECRETS_SERVICE_ACCOUNT"] +@pytest.fixture() +def topic_name() -> str: + return os.environ["GOOGLE_CLOUD_TOPIC_NAME"] + + +@pytest.fixture() +def rotation_period_hours() -> int: + return 24 + + @pytest.fixture() def ttl() -> Optional[str]: return "300s" @@ -840,7 +854,9 @@ def test_update_secret_expiration( # Update expire time to 2 hours - new_expire = datetime.now(timezone.utc) + timedelta(hours=2) # 2 hours from now in seconds + new_expire = datetime.now(timezone.utc) + timedelta( + hours=2 + ) # 2 hours from now in seconds update_secret_expiration(project_id, secret_id) # Verify output contains expected message @@ -876,3 +892,110 @@ def test_delete_expiration( assert ( retrieved_secret.expire_time is None ), f"ExpireTime is {retrieved_secret.expire_time}, expected None" + + +def test_create_secret_with_rotation( + capsys: pytest.LogCaptureFixture, + project_id: str, + secret_id: str, + topic_name: str, + rotation_period_hours: int, +) -> None: + """Test creating a secret with rotation configuration.""" + + # Create the secret with rotation + + create_secret_with_rotation(project_id, secret_id, topic_name) + + # Verify output contains expected message + + out, _ = capsys.readouterr() + assert "Created secret" in out, f"Expected 'Created secret' in output, got: {out}" + + retrieved_secret = get_secret(project_id, secret_id) + + # Verify rotation is configured + + assert retrieved_secret.rotation is not None, "Rotation is None, expected non-None" + + # Verify rotation period is set correctly (24 hours = 86400 seconds) + + expected_seconds = rotation_period_hours * 3600 + actual_seconds = retrieved_secret.rotation.rotation_period.total_seconds() + assert ( + actual_seconds == expected_seconds + ), f"RotationPeriod mismatch: got {actual_seconds}, want {expected_seconds}" + + # Verify next rotation time is set + + assert ( + retrieved_secret.rotation.next_rotation_time is not None + ), "NextRotationTime is None, expected non-None" + + +def test_update_secret_rotation_period( + capsys: pytest.LogCaptureFixture, project_id: str, secret_id: str, topic_name: str +) -> None: + + create_secret_with_rotation(project_id, secret_id, topic_name) + capsys.readouterr() + + updated_rotation_hours = 48 + update_secret_rotation(project_id, secret_id) + + # Verify output contains the secret ID + + out, _ = capsys.readouterr() + assert secret_id in out, f"Expected '{secret_id}' in output, got: {out}" + + retrieved_secret = get_secret(project_id, secret_id) + assert ( + retrieved_secret.rotation is not None + ), "GetSecret: Rotation is nil, expected non-nil" + expected_seconds = updated_rotation_hours * 3600 + actual_seconds = retrieved_secret.rotation.rotation_period.total_seconds() + assert ( + actual_seconds == expected_seconds + ), f"RotationPeriod mismatch: got {actual_seconds}, want {expected_seconds}" + + +def test_delete_secret_rotation( + capsys: pytest.LogCaptureFixture, project_id: str, secret_id: str, topic_name: str +) -> None: + + create_secret_with_rotation(project_id, secret_id, topic_name) + + # Delete the rotation + + delete_secret_rotation(project_id, secret_id) + out, _ = capsys.readouterr() + assert "Removed rotation from secret" in out + assert secret_id in out + + retrieved_secret = get_secret(project_id, secret_id) + assert ( + not retrieved_secret.rotation + ), f"Rotation is {repr(retrieved_secret.rotation)}, expected None or empty" + + +def test_create_secret_with_topic( + capsys, project_id: str, secret_id: str, topic_name: str +): + + # Call the function being tested + + create_secret_with_topic(project_id, secret_id, topic_name) + + # Check the output contains expected text + + out, _ = capsys.readouterr() + assert "Created secret" in out + + retrived_secret = get_secret(project_id, secret_id) + + assert ( + len(retrived_secret.topics) == 1 + ), f"Expected 1 topic, got {len(retrived_secret.topics)}" + assert ( + retrived_secret.topics[0].name == topic_name + ), f"Topic mismatch: got {retrived_secret.topics[0].name}, want {topic_name}" diff --git a/secretmanager/snippets/update_secret_rotation.py b/secretmanager/snippets/update_secret_rotation.py new file mode 100644 index 00000000000..31cfe98e2bc --- /dev/null +++ b/secretmanager/snippets/update_secret_rotation.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_update_secret_rotation] +from google.cloud import secretmanager +from google.protobuf import duration_pb2 +from google.protobuf.field_mask_pb2 import FieldMask + + +def update_secret_rotation(project_id: str, secret_id: str) -> None: + """ + Updates the rotation period of a secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to update + rotation_period_hours (int): New rotation period in hours + Example: + # Update the rotation period of a secret to 60 days + update_secret_rotation( + "my-project", + "my-secret-with-rotation", + "projects/my-project/topics/my-rotation-topic" + ) + """ + new_rotation_period_hours = 48 + # Create the Secret Manager client + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret + name = client.secret_path(project_id, secret_id) + + # Convert rotation period to protobuf Duration + rotation_period = duration_pb2.Duration() + rotation_period.seconds = ( + new_rotation_period_hours * 3600 + ) # Convert hours to seconds + + # Create the update mask + update_mask = FieldMask(paths=["rotation.rotation_period"]) + + # Build the request + request = { + "secret": { + "name": name, + "rotation": {"rotation_period": rotation_period}, + }, + "update_mask": update_mask, + } + + # Update the secret + result = client.update_secret(request=request) + + rotation_hours = result.rotation.rotation_period.seconds / 3600 + print(f"Updated secret {result.name} rotation period to {rotation_hours} hours") + + +# [END secretmanager_update_secret_rotation] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="ID of the GCP project") + parser.add_argument("secret_id", help="ID of the secret to update") + args = parser.parse_args() + + update_secret_rotation( + args.project_id, + args.secret_id, + ) diff --git a/testing/test-env.tmpl.sh b/testing/test-env.tmpl.sh index 1bfacfb2833..dc3b28ea4b0 100644 --- a/testing/test-env.tmpl.sh +++ b/testing/test-env.tmpl.sh @@ -80,6 +80,7 @@ export PUBSUB_VERIFICATION_TOKEN=1234abc # Secret Manager Test Vars export GCLOUD_SECRETS_SERVICE_ACCOUNT= +export GOOGLE_CLOUD_TOPIC_NAME= # Automl # A centralized project is used to remove duplicate work across all 7 languages From b4b8db962017aa4f7a9355b1ed75df786a44db88 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 10:49:06 +0530 Subject: [PATCH 10/24] feat(secretmanager): Update formatting --- secretmanager/snippets/create_secret_with_rotation.py | 11 ++++++----- .../create_regional_secret_with_rotation.py | 4 ++-- .../delete_regional_secret_rotation.py | 3 ++- .../snippets/regional_samples/snippets_test.py | 10 +++++----- .../update_regional_secret_rotation.py | 2 +- secretmanager/snippets/snippets_test.py | 10 +++++----- secretmanager/snippets/update_secret_rotation.py | 2 -- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/secretmanager/snippets/create_secret_with_rotation.py b/secretmanager/snippets/create_secret_with_rotation.py index 70a3dfe5a81..cdbd01ecf86 100644 --- a/secretmanager/snippets/create_secret_with_rotation.py +++ b/secretmanager/snippets/create_secret_with_rotation.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_secret_with_rotation] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.cloud import secretmanager from google.protobuf import duration_pb2 @@ -33,7 +33,7 @@ def create_secret_with_rotation( secret_id (str): ID of the secret to create topic_name (str): Resource name of the Pub/Sub topic for rotation notifications Example: - # Create a secret with automatic rotation every 30 days + # Create a secret with automatic rotation every 24 hours create_secret_with_rotation( "my-project", "my-rotating-secret", @@ -41,6 +41,7 @@ def create_secret_with_rotation( ) """ rotation_period_hours = 24 + next_rotation_time = datetime.now(timezone.utc) + timedelta(hours=24) # Create the Secret Manager client client = secretmanager.SecretManagerServiceClient() @@ -52,8 +53,8 @@ def create_secret_with_rotation( rotation_period.seconds = rotation_period_hours * 3600 # Convert hours to seconds # Set next rotation time to 24 hours from now - next_rotation_time = timestamp_pb2.Timestamp() - next_rotation_time.FromDatetime(datetime.now() + timedelta(hours=24)) + next_rotation_timestamp = timestamp_pb2.Timestamp() + next_rotation_timestamp.FromDatetime(next_rotation_time) # Create the secret with rotation configuration secret = client.create_secret( @@ -64,7 +65,7 @@ def create_secret_with_rotation( "replication": {"automatic": {}}, "topics": [{"name": topic_name}], "rotation": { - "next_rotation_time": next_rotation_time, + "next_rotation_time": next_rotation_timestamp, "rotation_period": rotation_period, }, }, diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py index f9a88a97007..6cf982fff1d 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_regional_secret_with_rotation] -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from google.api_core import client_options from google.cloud import secretmanager @@ -48,7 +48,7 @@ def create_regional_secret_with_rotation( rotation_period = timedelta(hours=24) # Set next rotation time to 24 hours from now - next_rotation_time = datetime.now() + timedelta(hours=24) + next_rotation_time = datetime.now(timezone.utc) + timedelta(hours=24) # Set up the endpoint for the specific region endpoint = f"secretmanager.{location_id}.rep.googleapis.com" diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py index 6ec767b2ef4..6e99074849a 100644 --- a/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse # [START secretmanager_delete_regional_secret_rotation] from google.api_core import client_options @@ -75,6 +74,8 @@ def delete_regional_secret_rotation( if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 04f0e534246..6f14116f21e 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -1160,13 +1160,13 @@ def test_create_regional_secret_with_topic( out, _ = capsys.readouterr() assert "Created secret" in out - retrived_secret = get_regional_secret.get_regional_secret( + retrieved_secret = get_regional_secret.get_regional_secret( project_id, location_id, secret_id ) assert ( - len(retrived_secret.topics) == 1 - ), f"Expected 1 topic, got {len(retrived_secret.topics)}" + len(retrieved_secret.topics) == 1 + ), f"Expected 1 topic, got {len(retrieved_secret.topics)}" assert ( - retrived_secret.topics[0].name == topic_name - ), f"Topic mismatch: got {retrived_secret.topics[0].name}, want {topic_name}" + retrieved_secret.topics[0].name == topic_name + ), f"Topic mismatch: got {retrieved_secret.topics[0].name}, want {topic_name}" diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py index 56ab0df8ec9..8445fdd5ca3 100644 --- a/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py +++ b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse # [START secretmanager_update_regional_secret_rotation_period] from google.api_core import client_options @@ -90,6 +89,7 @@ def update_regional_secret_rotation_period( if __name__ == "__main__": + import argparse parser = argparse.ArgumentParser( description=__doc__, diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index fea045f4938..d5503344101 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -991,11 +991,11 @@ def test_create_secret_with_topic( out, _ = capsys.readouterr() assert "Created secret" in out - retrived_secret = get_secret(project_id, secret_id) + retrieved_secret = get_secret(project_id, secret_id) assert ( - len(retrived_secret.topics) == 1 - ), f"Expected 1 topic, got {len(retrived_secret.topics)}" + len(retrieved_secret.topics) == 1 + ), f"Expected 1 topic, got {len(retrieved_secret.topics)}" assert ( - retrived_secret.topics[0].name == topic_name - ), f"Topic mismatch: got {retrived_secret.topics[0].name}, want {topic_name}" + retrieved_secret.topics[0].name == topic_name + ), f"Topic mismatch: got {retrieved_secret.topics[0].name}, want {topic_name}" diff --git a/secretmanager/snippets/update_secret_rotation.py b/secretmanager/snippets/update_secret_rotation.py index 31cfe98e2bc..0ba8c60b177 100644 --- a/secretmanager/snippets/update_secret_rotation.py +++ b/secretmanager/snippets/update_secret_rotation.py @@ -27,13 +27,11 @@ def update_secret_rotation(project_id: str, secret_id: str) -> None: Args: project_id (str): ID of the Google Cloud project secret_id (str): ID of the secret to update - rotation_period_hours (int): New rotation period in hours Example: # Update the rotation period of a secret to 60 days update_secret_rotation( "my-project", "my-secret-with-rotation", - "projects/my-project/topics/my-rotation-topic" ) """ new_rotation_period_hours = 48 From fd227b1ea7eeec6e508bda954b4e9c1fdf3ca07d Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 10:55:36 +0530 Subject: [PATCH 11/24] feat(secretmanager): Use flags instead of count --- secretmanager/snippets/list_tag_bindings.py | 6 +++--- .../regional_samples/list_regional_secret_tag_bindings.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py index 887b87e8d84..9bed66e63b1 100644 --- a/secretmanager/snippets/list_tag_bindings.py +++ b/secretmanager/snippets/list_tag_bindings.py @@ -50,14 +50,14 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: # Retrieve and process tag bindings bindings = client.list_tag_bindings(request=request) - count = 0 + found_bindings = False print(f"Tag bindings for {secret_name}:") for binding in bindings: print(f"- Tag Value: {binding.tag_value}") - count += 1 + found_bindings = True - if count == 0: + if not found_bindings: print(f"No tag bindings found for {secret_name}.") diff --git a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py index 569206c4f9f..ee50eec70cd 100644 --- a/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py +++ b/secretmanager/snippets/regional_samples/list_regional_secret_tag_bindings.py @@ -53,7 +53,7 @@ def list_regional_secret_tag_bindings( # List the tag bindings print(f"Tag bindings for {name}:") - count = 0 + found_bindings = False # Use the list_tag_bindings method to get all tag bindings request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) @@ -62,9 +62,9 @@ def list_regional_secret_tag_bindings( # Iterate through the results for binding in tag_bindings: print(f"- Tag Value: {binding.tag_value}") - count += 1 + found_bindings = True - if count == 0: + if not found_bindings: print(f"No tag bindings found for {name}.") From 6d10ee65544737f189591affda6aa22cb5505802 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 11:27:29 +0530 Subject: [PATCH 12/24] feat(secretmanager): Add cmek samples in python --- .../snippets/create_secret_with_cmek.py | 84 +++++++++++++++++ .../create_regional_secret_with_cmek.py | 94 +++++++++++++++++++ .../regional_samples/snippets_test.py | 47 ++++++++++ .../update_regional_secret_with_alias.py | 91 ++++++++++++++++++ secretmanager/snippets/snippets_test.py | 33 +++++++ testing/test-env.tmpl.sh | 2 + 6 files changed, 351 insertions(+) create mode 100644 secretmanager/snippets/create_secret_with_cmek.py create mode 100644 secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py create mode 100644 secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py diff --git a/secretmanager/snippets/create_secret_with_cmek.py b/secretmanager/snippets/create_secret_with_cmek.py new file mode 100644 index 00000000000..7ed7892b873 --- /dev/null +++ b/secretmanager/snippets/create_secret_with_cmek.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + +# [START secretmanager_create_secret_with_cmek] +from google.cloud import secretmanager + + +def create_secret_with_cmek( + project_id: str, secret_id: str, kms_key_name: str +) -> None: + """ + Creates a new secret with a customer-managed encryption key (CMEK). + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + kms_key_name (str): Full resource name of the KMS key + (e.g., "projects/my-project/locations/global/keyRings/{keyringname}/cryptoKeys/{keyname}") + + Example: + # Create a secret with a customer-managed encryption key + create_secret_with_cmek( + "my-project", + "my-secret-with-cmek", + "projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key" + ) + """ + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the parent project. + parent = f"projects/{project_id}" + + # Create the secret with automatic replication and CMEK. + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "replication": { + "automatic": { + "customer_managed_encryption": { + "kms_key_name": kms_key_name + } + } + } + }, + } + ) + + print(f"Created secret {secret.name} with CMEK key {kms_key_name}") + + +# [END secretmanager_create_secret_with_cmek] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "kms_key_name", help="full resource name of the KMS key" + ) + args = parser.parse_args() + + create_secret_with_cmek(args.project_id, args.secret_id, args.kms_key_name) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py new file mode 100644 index 00000000000..c82387047a4 --- /dev/null +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + + +# [START secretmanager_create_regional_secret_with_cmek] +from google.api_core import client_options +from google.cloud import secretmanager + + +def create_regional_secret_with_cmek( + project_id: str, secret_id: str, location_id: str, kms_key_name: str +) -> None: + """ + Creates a new regional secret encrypted with a customer-managed encryption key (CMEK). + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret to create + location_id (str): Region where the secret should be stored (e.g., "us-central1") + kms_key_name (str): Full resource name of the KMS key + (e.g., "projects/my-project/locations/{location_id}/keyRings/{keyringname}/cryptoKeys/{keyname}") + + Example: + # Create a regional secret with a customer-managed encryption key + create_regional_secret_with_cmek( + "my-project", + "my-regional-secret-with-cmek", + "us-central1", + "projects/my-project/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key" + ) + """ + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Build the resource name of the parent project with location + parent = f"projects/{project_id}/locations/{location_id}" + + # Create the secret with CMEK + secret = client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": { + "customer_managed_encryption": {"kms_key_name": kms_key_name} + }, + } + ) + + print(f"Created secret {secret.name} with CMEK key {kms_key_name}") + + +# [END secretmanager_create_regional_secret_with_cmek] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to create") + parser.add_argument( + "location_id", + help="region where the secret should be stored (e.g., us-central1)", + ) + parser.add_argument( + "kms_key_name", help="full resource name of the KMS key" + ) + args = parser.parse_args() + + create_regional_secret_with_cmek( + args.project_id, args.secret_id, args.location_id, args.kms_key_name + ) diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 6f14116f21e..fe37e84bc47 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -28,6 +28,7 @@ from regional_samples import bind_tags_to_regional_secret from regional_samples import create_regional_secret from regional_samples import create_regional_secret_with_annotations +from regional_samples import create_regional_secret_with_cmek from regional_samples import create_regional_secret_with_delayed_destroy from regional_samples import create_regional_secret_with_expire_time from regional_samples import create_regional_secret_with_labels @@ -63,6 +64,7 @@ from regional_samples import update_regional_secret from regional_samples import update_regional_secret_expiration from regional_samples import update_regional_secret_rotation +from regional_samples import update_regional_secret_with_alias from regional_samples import update_regional_secret_with_delayed_destroy from regional_samples import update_regional_secret_with_etag from regional_samples import view_regional_secret_annotations @@ -117,6 +119,11 @@ def topic_name() -> str: return os.environ["GOOGLE_CLOUD_TOPIC_NAME"] +@pytest.fixture() +def kms_key_name() -> str: + return os.environ["GOOGLE_CLOUD_REGIONAL_KMS_KEY_NAME"] + + @pytest.fixture() def rotation_period_hours() -> int: return 24 @@ -1170,3 +1177,43 @@ def test_create_regional_secret_with_topic( assert ( retrieved_secret.topics[0].name == topic_name ), f"Topic mismatch: got {retrieved_secret.topics[0].name}, want {topic_name}" + + +def test_create_regional_secret_with_cmek( + capsys, project_id: str, secret_id: str, location_id: str, kms_key_name: str +): + + create_regional_secret_with_cmek.create_regional_secret_with_cmek( + project_id, secret_id, location_id, kms_key_name + ) + + # Check the output contains expected text + + out, _ = capsys.readouterr() + assert "Created secret" in out + assert secret_id in out + assert kms_key_name in out + + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + + # Check that the CMEK key name matches what we specified + + actual_key_name = retrieved_secret.customer_managed_encryption.kms_key_name + assert ( + actual_key_name == kms_key_name + ), f"CMEK key name mismatch: got {actual_key_name}, want {kms_key_name}" + + +def test_update_regional_secret_with_alias( + project_id: str, location_id: str, regional_secret_version: Tuple[str, str, str] +) -> None: + secret_id, _, _ = regional_secret_version + update_regional_secret_with_alias.update_regional_secret_with_alias( + project_id, secret_id, location_id + ) + retrieved_secret = get_regional_secret.get_regional_secret( + project_id, location_id, secret_id + ) + assert retrieved_secret.version_aliases["test"] == 1 diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py b/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py new file mode 100644 index 00000000000..d0bce6dd04d --- /dev/null +++ b/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +# Copyright 2026 Google LLC +# +# 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. + + +# [START secretmanager_update_regional_secret_with_alias] +from google.api_core import client_options +from google.cloud import secretmanager +from google.protobuf import field_mask_pb2 + + +def update_regional_secret_with_alias( + project_id: str, secret_id: str, location_id: str +) -> None: + """ + Updates the alias map on an existing regional secret. + + Args: + project_id (str): ID of the Google Cloud project + secret_id (str): ID of the secret + location_id (str): Region where the secret is stored (e.g., "us-central1") + + Example: + # Update a regional secret with an alias + update_regional_secret_with_alias( + "my-project", + "my-secret", + "us-central1" + ) + """ + # Construct the secret name from the component parts + secret_name = ( + f"projects/{project_id}/locations/{location_id}/secrets/{secret_id}" + ) + + # Set up the endpoint for the specific region + endpoint = f"secretmanager.{location_id}.rep.googleapis.com" + client_option = client_options.ClientOptions(api_endpoint=endpoint) + + # Create the Secret Manager client with the regional endpoint + client = secretmanager.SecretManagerServiceClient( + client_options=client_option + ) + + # Create a field mask to update only the version_aliases field + update_mask = field_mask_pb2.FieldMask(paths=["version_aliases"]) + + # Update the secret with the new alias map + result = client.update_secret( + request={ + "secret": {"name": secret_name, "version_aliases": {"test": 1}}, + "update_mask": update_mask, + } + ) + + print(f"Updated regional secret: {result.name}") + + +# [END secretmanager_update_regional_secret_with_alias] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret") + parser.add_argument( + "location_id", + help="region where the secret is stored (e.g., us-central1)", + ) + args = parser.parse_args() + + update_regional_secret_with_alias( + args.project_id, args.secret_id, args.location_id + ) diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index d5503344101..a79c02bde25 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -31,6 +31,7 @@ from consume_event_notification import consume_event_notification from create_secret import create_secret from create_secret_with_annotations import create_secret_with_annotations +from create_secret_with_cmek import create_secret_with_cmek from create_secret_with_delayed_destroy import create_secret_with_delayed_destroy from create_secret_with_expiration import create_secret_with_expiration from create_secret_with_labels import create_secret_with_labels @@ -109,6 +110,11 @@ def rotation_period_hours() -> int: return 24 +@pytest.fixture() +def kms_key_name() -> str: + return os.environ["GOOGLE_CLOUD_KMS_KEY_NAME"] + + @pytest.fixture() def ttl() -> Optional[str]: return "300s" @@ -999,3 +1005,30 @@ def test_create_secret_with_topic( assert ( retrieved_secret.topics[0].name == topic_name ), f"Topic mismatch: got {retrieved_secret.topics[0].name}, want {topic_name}" + + +def test_create_secret_with_cmek( + capsys, project_id: str, secret_id: str, kms_key_name: str +): + + create_secret_with_cmek(project_id, secret_id, kms_key_name) + + # Check the output contains expected text + + out, _ = capsys.readouterr() + assert "Created secret" in out + assert secret_id in out + assert kms_key_name in out + + # Verify CMEK key with GetSecret + + retrieved_secret = get_secret(project_id, secret_id) + + # Check that the CMEK key name matches what we specified + + actual_key_name = ( + retrieved_secret.replication.automatic.customer_managed_encryption.kms_key_name + ) + assert ( + actual_key_name == kms_key_name + ), f"CMEK key name mismatch: got {actual_key_name}, want {kms_key_name}" diff --git a/testing/test-env.tmpl.sh b/testing/test-env.tmpl.sh index dc3b28ea4b0..938e12352ac 100644 --- a/testing/test-env.tmpl.sh +++ b/testing/test-env.tmpl.sh @@ -81,6 +81,8 @@ export PUBSUB_VERIFICATION_TOKEN=1234abc # Secret Manager Test Vars export GCLOUD_SECRETS_SERVICE_ACCOUNT= export GOOGLE_CLOUD_TOPIC_NAME= +export GOOGLE_CLOUD_KMS_KEY_NAME= +export GOOGLE_CLOUD_REGIONAL_KMS_KEY_NAME= # Automl # A centralized project is used to remove duplicate work across all 7 languages From 6c19a869fb1af9e328ae643a344e33f5216c777e Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 12:11:44 +0530 Subject: [PATCH 13/24] feat(secretmanager): Use v1 lib --- secretmanager/snippets/list_tag_bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/secretmanager/snippets/list_tag_bindings.py b/secretmanager/snippets/list_tag_bindings.py index 9bed66e63b1..240d626a830 100644 --- a/secretmanager/snippets/list_tag_bindings.py +++ b/secretmanager/snippets/list_tag_bindings.py @@ -20,7 +20,7 @@ # [START secretmanager_list_tag_bindings] # Import the Resource Manager client library. from google.cloud import resourcemanager_v3 -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 def list_tag_bindings(project_id: str, secret_id: str) -> None: @@ -38,7 +38,7 @@ def list_tag_bindings(project_id: str, secret_id: str) -> None: # Create the Resource Manager client. client = resourcemanager_v3.TagBindingsClient() - sm_client = secretmanager.SecretManagerServiceClient() + sm_client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the parent secret. secret_name = sm_client.secret_path(project_id, secret_id) From 31e04a79bbb1aedd422df93fe205f995438ea9b3 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 12:25:10 +0530 Subject: [PATCH 14/24] feat(secretmanager): Use v1 lib --- secretmanager/snippets/create_secret_with_expiration.py | 4 ++-- secretmanager/snippets/delete_secret_expiration.py | 4 ++-- .../create_regional_secret_with_expire_time.py | 4 ++-- .../regional_samples/delete_regional_secret_expiration.py | 4 ++-- .../regional_samples/update_regional_secret_expiration.py | 4 ++-- secretmanager/snippets/update_secret_expiration.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/secretmanager/snippets/create_secret_with_expiration.py b/secretmanager/snippets/create_secret_with_expiration.py index 9af6d04dd6d..e80d1bf7b83 100644 --- a/secretmanager/snippets/create_secret_with_expiration.py +++ b/secretmanager/snippets/create_secret_with_expiration.py @@ -17,7 +17,7 @@ # [START secretmanager_create_secret_with_expiration] from datetime import datetime, timedelta, timezone -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import timestamp_pb2 @@ -35,7 +35,7 @@ def create_secret_with_expiration(project_id: str, secret_id: str) -> None: """ expire_time = datetime.now(timezone.utc) + timedelta(hours=1) # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the parent project. parent = f"projects/{project_id}" diff --git a/secretmanager/snippets/delete_secret_expiration.py b/secretmanager/snippets/delete_secret_expiration.py index 066d2f6fcba..c3f3f8bbf60 100644 --- a/secretmanager/snippets/delete_secret_expiration.py +++ b/secretmanager/snippets/delete_secret_expiration.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_delete_secret_expiration] -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf.field_mask_pb2 import FieldMask @@ -34,7 +34,7 @@ def delete_secret_expiration(project_id: str, secret_id: str) -> None: ) """ # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the secret name = client.secret_path(project_id, secret_id) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py index 9d35fe1f765..f7a153fc7c0 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, timezone from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import timestamp_pb2 @@ -45,7 +45,7 @@ def create_regional_secret_with_expire_time( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py index c69a6a0c8a4..eff41569104 100644 --- a/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_expiration.py @@ -17,7 +17,7 @@ # [START secretmanager_delete_regional_secret_expiration] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import field_mask_pb2 @@ -50,7 +50,7 @@ def delete_regional_secret_expiration( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py index 74f83566127..6f137064a8b 100644 --- a/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py +++ b/secretmanager/snippets/regional_samples/update_regional_secret_expiration.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, timezone from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import field_mask_pb2, timestamp_pb2 @@ -54,7 +54,7 @@ def update_regional_secret_expiration( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/update_secret_expiration.py b/secretmanager/snippets/update_secret_expiration.py index cd460cb82e6..730c8e06987 100644 --- a/secretmanager/snippets/update_secret_expiration.py +++ b/secretmanager/snippets/update_secret_expiration.py @@ -18,7 +18,7 @@ # [START secretmanager_update_secret_expiration] from datetime import datetime, timedelta, timezone -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import timestamp_pb2 @@ -40,7 +40,7 @@ def update_secret_expiration(project_id: str, secret_id: str) -> None: new_expire_time = datetime.now(timezone.utc) + timedelta(hours=2) # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the secret. name = client.secret_path(project_id, secret_id) From ccfa951aaaed2be86f7e497373969b997b5a588b Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 12:39:12 +0530 Subject: [PATCH 15/24] feat(secretmanager): Use v1 lib --- secretmanager/snippets/create_secret_with_rotation.py | 4 ++-- secretmanager/snippets/create_secret_with_topic.py | 4 ++-- secretmanager/snippets/delete_secret_rotation.py | 4 ++-- .../regional_samples/create_regional_secret_with_rotation.py | 4 ++-- .../regional_samples/create_regional_secret_with_topic.py | 4 ++-- .../regional_samples/delete_regional_secret_rotation.py | 4 ++-- .../regional_samples/update_regional_secret_rotation.py | 4 ++-- secretmanager/snippets/update_secret_rotation.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/secretmanager/snippets/create_secret_with_rotation.py b/secretmanager/snippets/create_secret_with_rotation.py index cdbd01ecf86..a189a2c8443 100644 --- a/secretmanager/snippets/create_secret_with_rotation.py +++ b/secretmanager/snippets/create_secret_with_rotation.py @@ -17,7 +17,7 @@ # [START secretmanager_create_secret_with_rotation] from datetime import datetime, timedelta, timezone -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import duration_pb2 from google.protobuf import timestamp_pb2 @@ -43,7 +43,7 @@ def create_secret_with_rotation( rotation_period_hours = 24 next_rotation_time = datetime.now(timezone.utc) + timedelta(hours=24) # Create the Secret Manager client - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the parent project parent = f"projects/{project_id}" diff --git a/secretmanager/snippets/create_secret_with_topic.py b/secretmanager/snippets/create_secret_with_topic.py index b161c167fb0..2db1eb47851 100644 --- a/secretmanager/snippets/create_secret_with_topic.py +++ b/secretmanager/snippets/create_secret_with_topic.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_secret_with_topic] -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 def create_secret_with_topic(project_id: str, secret_id: str, topic_name: str) -> None: @@ -35,7 +35,7 @@ def create_secret_with_topic(project_id: str, secret_id: str, topic_name: str) - ) """ # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the parent name. parent = f"projects/{project_id}" diff --git a/secretmanager/snippets/delete_secret_rotation.py b/secretmanager/snippets/delete_secret_rotation.py index d94770e833b..6bc7106f007 100644 --- a/secretmanager/snippets/delete_secret_rotation.py +++ b/secretmanager/snippets/delete_secret_rotation.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_delete_secret_rotation] -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf.field_mask_pb2 import FieldMask @@ -34,7 +34,7 @@ def delete_secret_rotation(project_id: str, secret_id: str) -> None: ) """ # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the secret name = client.secret_path(project_id, secret_id) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py index 6cf982fff1d..7eba72cf7f3 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, timezone from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import duration_pb2, timestamp_pb2 @@ -55,7 +55,7 @@ def create_regional_secret_with_rotation( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py index a52ddf1daa1..ef8c80cd8e5 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py @@ -16,7 +16,7 @@ # [START secretmanager_create_regional_secret_with_topic] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 def create_regional_secret_with_topic( @@ -46,7 +46,7 @@ def create_regional_secret_with_topic( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py index 6e99074849a..d3833b09f96 100644 --- a/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py +++ b/secretmanager/snippets/regional_samples/delete_regional_secret_rotation.py @@ -17,7 +17,7 @@ # [START secretmanager_delete_regional_secret_rotation] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import field_mask_pb2 @@ -50,7 +50,7 @@ def delete_regional_secret_rotation( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py index 8445fdd5ca3..662fa2ac21c 100644 --- a/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py +++ b/secretmanager/snippets/regional_samples/update_regional_secret_rotation.py @@ -17,7 +17,7 @@ # [START secretmanager_update_regional_secret_rotation_period] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import duration_pb2, field_mask_pb2 @@ -53,7 +53,7 @@ def update_regional_secret_rotation_period( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/update_secret_rotation.py b/secretmanager/snippets/update_secret_rotation.py index 0ba8c60b177..ae2b3cc3208 100644 --- a/secretmanager/snippets/update_secret_rotation.py +++ b/secretmanager/snippets/update_secret_rotation.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_update_secret_rotation] -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import duration_pb2 from google.protobuf.field_mask_pb2 import FieldMask @@ -36,7 +36,7 @@ def update_secret_rotation(project_id: str, secret_id: str) -> None: """ new_rotation_period_hours = 48 # Create the Secret Manager client - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the secret name = client.secret_path(project_id, secret_id) From dd8e42d684ae8d4f5904cfeae290a367f8515a41 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 12:45:34 +0530 Subject: [PATCH 16/24] feat(secretmanager): Use v1 lib --- secretmanager/snippets/create_secret_with_cmek.py | 4 ++-- .../regional_samples/create_regional_secret_with_cmek.py | 4 ++-- .../regional_samples/update_regional_secret_with_alias.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/secretmanager/snippets/create_secret_with_cmek.py b/secretmanager/snippets/create_secret_with_cmek.py index 7ed7892b873..1fbd27fa891 100644 --- a/secretmanager/snippets/create_secret_with_cmek.py +++ b/secretmanager/snippets/create_secret_with_cmek.py @@ -15,7 +15,7 @@ # limitations under the License. # [START secretmanager_create_secret_with_cmek] -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 def create_secret_with_cmek( @@ -39,7 +39,7 @@ def create_secret_with_cmek( ) """ # Create the Secret Manager client. - client = secretmanager.SecretManagerServiceClient() + client = secretmanager_v1.SecretManagerServiceClient() # Build the resource name of the parent project. parent = f"projects/{project_id}" diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py index c82387047a4..86dbdeb7bcc 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py @@ -17,7 +17,7 @@ # [START secretmanager_create_regional_secret_with_cmek] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 def create_regional_secret_with_cmek( @@ -47,7 +47,7 @@ def create_regional_secret_with_cmek( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) diff --git a/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py b/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py index d0bce6dd04d..09eb07124fa 100644 --- a/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py +++ b/secretmanager/snippets/regional_samples/update_regional_secret_with_alias.py @@ -17,7 +17,7 @@ # [START secretmanager_update_regional_secret_with_alias] from google.api_core import client_options -from google.cloud import secretmanager +from google.cloud import secretmanager_v1 from google.protobuf import field_mask_pb2 @@ -50,7 +50,7 @@ def update_regional_secret_with_alias( client_option = client_options.ClientOptions(api_endpoint=endpoint) # Create the Secret Manager client with the regional endpoint - client = secretmanager.SecretManagerServiceClient( + client = secretmanager_v1.SecretManagerServiceClient( client_options=client_option ) From a609fc5c8bde5f341a620757165d7394171d1ef1 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 13:45:51 +0530 Subject: [PATCH 17/24] feat(secretmanager): Use path --- .../regional_samples/create_regional_secret_with_cmek.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py index 86dbdeb7bcc..9093f546beb 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_cmek.py @@ -52,7 +52,7 @@ def create_regional_secret_with_cmek( ) # Build the resource name of the parent project with location - parent = f"projects/{project_id}/locations/{location_id}" + parent = client.common_location_path(project_id, location_id) # Create the secret with CMEK secret = client.create_secret( From 9ba1e1a1b9ea99d0b8cbbe0919dc3de06df4bddc Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 13:48:36 +0530 Subject: [PATCH 18/24] feat(secretmanager): Use path --- .../regional_samples/create_regional_secret_with_expire_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py index f7a153fc7c0..e00eef48b12 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_expire_time.py @@ -50,7 +50,7 @@ def create_regional_secret_with_expire_time( ) # Build the resource name of the parent project with location - parent = f"projects/{project_id}/locations/{location_id}" + parent = client.common_location_path(project_id, location_id) # Convert the Python datetime to a Protobuf Timestamp timestamp = timestamp_pb2.Timestamp() From 857c9fa71e52744cfad0711c37f99d246e36a428 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 13:51:13 +0530 Subject: [PATCH 19/24] feat(secretmanager): Use path --- .../regional_samples/create_regional_secret_with_rotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py index 7eba72cf7f3..70c181762ba 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_rotation.py @@ -60,7 +60,7 @@ def create_regional_secret_with_rotation( ) # Build the resource name of the parent project with location - parent = f"projects/{project_id}/locations/{location_id}" + parent = client.common_location_path(project_id, location_id) # Convert the Python datetime to a Protobuf Timestamp next_rotation_timestamp = timestamp_pb2.Timestamp() From ecc1bd5c6af538771595272f3a309d1522ebf1c3 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 13:54:00 +0530 Subject: [PATCH 20/24] feat(secretmanager): Use path --- .../regional_samples/create_regional_secret_with_topic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py index ef8c80cd8e5..bc802f854c7 100644 --- a/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py +++ b/secretmanager/snippets/regional_samples/create_regional_secret_with_topic.py @@ -51,7 +51,7 @@ def create_regional_secret_with_topic( ) # Build the resource name of the parent project with location - parent = f"projects/{project_id}/locations/{location_id}" + parent = client.common_location_path(project_id, location_id) # Create the secret with a topic for notifications secret = client.create_secret( From 008e07e2c24f17dba7ea91451625c4b9211b5ee5 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 14:24:42 +0530 Subject: [PATCH 21/24] feat(secretmanager): Update formatting --- secretmanager/snippets/create_secret_with_cmek.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secretmanager/snippets/create_secret_with_cmek.py b/secretmanager/snippets/create_secret_with_cmek.py index 1fbd27fa891..56bf73e4523 100644 --- a/secretmanager/snippets/create_secret_with_cmek.py +++ b/secretmanager/snippets/create_secret_with_cmek.py @@ -22,7 +22,7 @@ def create_secret_with_cmek( project_id: str, secret_id: str, kms_key_name: str ) -> None: """ - Creates a new secret with a customer-managed encryption key (CMEK). + Creates a new secret with a customer-managed encryption key (CMEK). Args: project_id (str): ID of the Google Cloud project From e3db20a4173690d9560a49bc868eb1d08bd80064 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 14:30:39 +0530 Subject: [PATCH 22/24] feat(secretmanager): Use path --- secretmanager/snippets/detach_tag_binding.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/secretmanager/snippets/detach_tag_binding.py b/secretmanager/snippets/detach_tag_binding.py index 438545361f2..56b3adcd7d6 100644 --- a/secretmanager/snippets/detach_tag_binding.py +++ b/secretmanager/snippets/detach_tag_binding.py @@ -17,7 +17,7 @@ # [START secretmanager_detach_tag_binding] from google.cloud import resourcemanager_v3 - +from google.cloud import secretmanager_v1 def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: """ @@ -33,17 +33,18 @@ def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: detach_tag("my-project", "my-secret", "tagValues/123456789012") """ # Create the Resource Manager client. - client = resourcemanager_v3.TagBindingsClient() + rm_client = resourcemanager_v3.TagBindingsClient() # Build the resource name of the parent secret. - secret_name = f"projects/{project_id}/secrets/{secret_id}" + client = secretmanager_v1.SecretManagerServiceClient() + secret_name = client.secret_path(project_id, secret_id) parent = f"//secretmanager.googleapis.com/{secret_name}" # Find the binding name for the given tag value binding_name = None request = resourcemanager_v3.ListTagBindingsRequest(parent=parent) - for binding in client.list_tag_bindings(request=request): + for binding in rm_client.list_tag_bindings(request=request): if binding.tag_value == tag_value: binding_name = binding.name break @@ -54,7 +55,7 @@ def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: # Delete the tag binding request = resourcemanager_v3.DeleteTagBindingRequest(name=binding_name) - operation = client.delete_tag_binding(request=request) + operation = rm_client.delete_tag_binding(request=request) # Wait for the operation to complete operation.result() From f93756a1adad5ac54b540ad30730eb8380dcd1ee Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 17:23:06 +0530 Subject: [PATCH 23/24] feat(secretmanager): Use rotation for comparison --- secretmanager/snippets/regional_samples/snippets_test.py | 2 +- secretmanager/snippets/snippets_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/secretmanager/snippets/regional_samples/snippets_test.py b/secretmanager/snippets/regional_samples/snippets_test.py index 6f14116f21e..c9e0477917b 100644 --- a/secretmanager/snippets/regional_samples/snippets_test.py +++ b/secretmanager/snippets/regional_samples/snippets_test.py @@ -1141,7 +1141,7 @@ def test_delete_regional_secret_rotation( # Check that rotation configuration is removed assert ( - not retrieved_secret.rotation + retrieved_secret.rotation == secretmanager_v1.types.Rotation() ), f"Rotation is {repr(retrieved_secret.rotation)}, expected None or empty" diff --git a/secretmanager/snippets/snippets_test.py b/secretmanager/snippets/snippets_test.py index d5503344101..da582ced8a9 100644 --- a/secretmanager/snippets/snippets_test.py +++ b/secretmanager/snippets/snippets_test.py @@ -20,7 +20,7 @@ from google.api_core import exceptions, retry from google.cloud import resourcemanager_v3 -from google.cloud import secretmanager +from google.cloud import secretmanager, secretmanager_v1 from google.protobuf.duration_pb2 import Duration import pytest @@ -974,7 +974,7 @@ def test_delete_secret_rotation( retrieved_secret = get_secret(project_id, secret_id) assert ( - not retrieved_secret.rotation + retrieved_secret.rotation == secretmanager_v1.types.Rotation() ), f"Rotation is {repr(retrieved_secret.rotation)}, expected None or empty" From 308099d4bf76b59e808765294e46cfb33b3904d5 Mon Sep 17 00:00:00 2001 From: Khilan Maradiya Date: Fri, 16 Jan 2026 17:24:56 +0530 Subject: [PATCH 24/24] feat(secretmanager): Update formatting --- secretmanager/snippets/detach_tag_binding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/secretmanager/snippets/detach_tag_binding.py b/secretmanager/snippets/detach_tag_binding.py index 56b3adcd7d6..78eee7b5524 100644 --- a/secretmanager/snippets/detach_tag_binding.py +++ b/secretmanager/snippets/detach_tag_binding.py @@ -19,6 +19,7 @@ from google.cloud import resourcemanager_v3 from google.cloud import secretmanager_v1 + def detach_tag(project_id: str, secret_id: str, tag_value: str) -> None: """ Detaches a tag value from a secret.