diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index e2b39f9..9ee60f7 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-python:latest - digest: sha256:99d90d097e4a4710cc8658ee0b5b963f4426d0e424819787c3ac1405c9a26719 + digest: sha256:aea14a583128771ae8aefa364e1652f3c56070168ef31beb203534222d842b8b diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 51957e6..41d64c0 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -5,10 +5,12 @@ branchProtectionRules: # Identifies the protection rule pattern. Name of the branch to be protected. # Defaults to `master` - pattern: master + requiresCodeOwnerReviews: true + requiresStrictStatusChecks: true requiredStatusCheckContexts: - 'Kokoro' - 'cla/google' - 'Samples - Lint' - 'Samples - Python 3.6' - 'Samples - Python 3.7' - - 'Samples - Python 3.8' \ No newline at end of file + - 'Samples - Python 3.8' diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 412b0b5..4e1b1fb 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -40,6 +40,7 @@ RUN apt-get update \ libssl-dev \ libsqlite3-dev \ portaudio19-dev \ + python3-distutils \ redis-server \ software-properties-common \ ssh \ @@ -59,40 +60,8 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb - -COPY fetch_gpg_keys.sh /tmp -# Install the desired versions of Python. -RUN set -ex \ - && export GNUPGHOME="$(mktemp -d)" \ - && echo "disable-ipv6" >> "${GNUPGHOME}/dirmngr.conf" \ - && /tmp/fetch_gpg_keys.sh \ - && for PYTHON_VERSION in 3.7.8 3.8.5; do \ - wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \ - && wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \ - && gpg --batch --verify python-${PYTHON_VERSION}.tar.xz.asc python-${PYTHON_VERSION}.tar.xz \ - && rm -r python-${PYTHON_VERSION}.tar.xz.asc \ - && mkdir -p /usr/src/python-${PYTHON_VERSION} \ - && tar -xJC /usr/src/python-${PYTHON_VERSION} --strip-components=1 -f python-${PYTHON_VERSION}.tar.xz \ - && rm python-${PYTHON_VERSION}.tar.xz \ - && cd /usr/src/python-${PYTHON_VERSION} \ - && ./configure \ - --enable-shared \ - # This works only on Python 2.7 and throws a warning on every other - # version, but seems otherwise harmless. - --enable-unicode=ucs4 \ - --with-system-ffi \ - --without-ensurepip \ - && make -j$(nproc) \ - && make install \ - && ldconfig \ - ; done \ - && rm -rf "${GNUPGHOME}" \ - && rm -rf /usr/src/python* \ - && rm -rf ~/.cache/ - RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ - && python3.7 /tmp/get-pip.py \ && python3.8 /tmp/get-pip.py \ && rm /tmp/get-pip.py -CMD ["python3.7"] +CMD ["python3.8"] diff --git a/.kokoro/docker/docs/fetch_gpg_keys.sh b/.kokoro/docker/docs/fetch_gpg_keys.sh deleted file mode 100755 index d653dd8..0000000 --- a/.kokoro/docker/docs/fetch_gpg_keys.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# Copyright 2020 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. - -# A script to fetch gpg keys with retry. -# Avoid jinja parsing the file. -# - -function retry { - if [[ "${#}" -le 1 ]]; then - echo "Usage: ${0} retry_count commands.." - exit 1 - fi - local retries=${1} - local command="${@:2}" - until [[ "${retries}" -le 0 ]]; do - $command && return 0 - if [[ $? -ne 0 ]]; then - echo "command failed, retrying" - ((retries--)) - fi - done - return 1 -} - -# 3.6.9, 3.7.5 (Ned Deily) -retry 3 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys \ - 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D - -# 3.8.0 (Ɓukasz Langa) -retry 3 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys \ - E3FF2839C048B25C084DEBE9B26995E310250568 - -# diff --git a/.kokoro/samples/python3.6/periodic-head.cfg b/.kokoro/samples/python3.6/periodic-head.cfg index f9cfcd3..b65f247 100644 --- a/.kokoro/samples/python3.6/periodic-head.cfg +++ b/.kokoro/samples/python3.6/periodic-head.cfg @@ -7,5 +7,5 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" + value: "github/python-secret-manager/.kokoro/test-samples-against-head.sh" } diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg index f9cfcd3..b65f247 100644 --- a/.kokoro/samples/python3.7/periodic-head.cfg +++ b/.kokoro/samples/python3.7/periodic-head.cfg @@ -7,5 +7,5 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" + value: "github/python-secret-manager/.kokoro/test-samples-against-head.sh" } diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg index f9cfcd3..b65f247 100644 --- a/.kokoro/samples/python3.8/periodic-head.cfg +++ b/.kokoro/samples/python3.8/periodic-head.cfg @@ -7,5 +7,5 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" + value: "github/python-secret-manager/.kokoro/test-samples-against-head.sh" } diff --git a/.kokoro/samples/python3.9/periodic-head.cfg b/.kokoro/samples/python3.9/periodic-head.cfg index f9cfcd3..b65f247 100644 --- a/.kokoro/samples/python3.9/periodic-head.cfg +++ b/.kokoro/samples/python3.9/periodic-head.cfg @@ -7,5 +7,5 @@ env_vars: { env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" + value: "github/python-secret-manager/.kokoro/test-samples-against-head.sh" } diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index cf5de74..311a8d5 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -20,9 +20,9 @@ set -eo pipefail # Enables `**` to include files nested inside sub-folders shopt -s globstar -# Exit early if samples directory doesn't exist -if [ ! -d "./samples" ]; then - echo "No tests run. `./samples` not found" +# Exit early if samples don't exist +if ! find samples -name 'requirements.txt' | grep -q .; then + echo "No tests run. './samples/**/requirements.txt' not found" exit 0 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b39ca3..c53b81d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [2.7.0](https://www.github.com/googleapis/python-secret-manager/compare/v2.6.0...v2.7.0) (2021-08-03) + + +### Features + +* add filter to customize the output of ListSecrets/ListSecretVersions calls ([#161](https://www.github.com/googleapis/python-secret-manager/issues/161)) ([c09615c](https://www.github.com/googleapis/python-secret-manager/commit/c09615c328782f0a15201cb4f2c7387b0a6ce51d)) + + +### Bug Fixes + +* **deps:** pin 'google-{api,cloud}-core', 'google-auth' to allow 2.x versions ([#153](https://www.github.com/googleapis/python-secret-manager/issues/153)) ([1e8a4aa](https://www.github.com/googleapis/python-secret-manager/commit/1e8a4aae06badda947717217c224366963664bdc)) +* enable self signed jwt for grpc ([#158](https://www.github.com/googleapis/python-secret-manager/issues/158)) ([9ebe2b3](https://www.github.com/googleapis/python-secret-manager/commit/9ebe2b3a683de1d710ec3e91b444eb71b2ef0f6b)) + + +### Documentation + +* **secretmanager:** add sample code for receiving a Pub/Sub message ([#138](https://www.github.com/googleapis/python-secret-manager/issues/138)) ([51f743d](https://www.github.com/googleapis/python-secret-manager/commit/51f743dfe2de41ef0378fff08c92c506dd11fc2b)) + + +### Miscellaneous Chores + +* release as 2.6.1 ([#159](https://www.github.com/googleapis/python-secret-manager/issues/159)) ([b686310](https://www.github.com/googleapis/python-secret-manager/commit/b686310643ec5fbd090a5d58d8a7694bdc6eebb9)) +* release as 2.7.0 ([#163](https://www.github.com/googleapis/python-secret-manager/issues/163)) ([b1c148b](https://www.github.com/googleapis/python-secret-manager/commit/b1c148bba25374bd9a62a6b823bf10ffd6215e9e)) + ## [2.6.0](https://www.github.com/googleapis/python-secret-manager/compare/v2.5.0...v2.6.0) (2021-07-09) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e8f3c78..48bf8fb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -177,6 +177,30 @@ Build the docs via: $ nox -s docs +************************* +Samples and code snippets +************************* + +Code samples and snippets live in the `samples/` catalogue. Feel free to +provide more examples, but make sure to write tests for those examples. +Each folder containing example code requires its own `noxfile.py` script +which automates testing. If you decide to create a new folder, you can +base it on the `samples/snippets` folder (providing `noxfile.py` and +the requirements files). + +The tests will run against a real Google Cloud Project, so you should +configure them just like the System Tests. + +- To run sample tests, you can execute:: + + # Run all tests in a folder + $ cd samples/snippets + $ nox -s py-3.8 + + # Run a single sample test + $ cd samples/snippets + $ nox -s py-3.8 -- -k + ******************************************** Note About ``README`` as it pertains to PyPI ******************************************** diff --git a/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index 90b459c..bba6e05 100644 --- a/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -383,6 +383,10 @@ def __init__( client_cert_source_for_mtls=client_cert_source_func, quota_project_id=client_options.quota_project_id, client_info=client_info, + always_use_jwt_access=( + Transport == type(self).get_transport_class("grpc") + or Transport == type(self).get_transport_class("grpc_asyncio") + ), ) def list_secrets( diff --git a/google/cloud/secretmanager_v1/types/service.py b/google/cloud/secretmanager_v1/types/service.py index 7ddeed3..0060ef3 100644 --- a/google/cloud/secretmanager_v1/types/service.py +++ b/google/cloud/secretmanager_v1/types/service.py @@ -59,11 +59,18 @@ class ListSecretsRequest(proto.Message): page_token (str): Optional. Pagination token, returned earlier via [ListSecretsResponse.next_page_token][google.cloud.secretmanager.v1.ListSecretsResponse.next_page_token]. + filter (str): + Optional. Filter string, adhering to the rules in + `List-operation + filtering `__. + List only secrets matching the filter. If filter is empty, + all secrets are listed. """ parent = proto.Field(proto.STRING, number=1,) page_size = proto.Field(proto.INT32, number=2,) page_token = proto.Field(proto.STRING, number=3,) + filter = proto.Field(proto.STRING, number=4,) class ListSecretsResponse(proto.Message): @@ -173,11 +180,18 @@ class ListSecretVersionsRequest(proto.Message): page_token (str): Optional. Pagination token, returned earlier via ListSecretVersionsResponse.next_page_token][]. + filter (str): + Optional. Filter string, adhering to the rules in + `List-operation + filtering `__. + List only secret versions matching the filter. If filter is + empty, all secret versions are listed. """ parent = proto.Field(proto.STRING, number=1,) page_size = proto.Field(proto.INT32, number=2,) page_token = proto.Field(proto.STRING, number=3,) + filter = proto.Field(proto.STRING, number=4,) class ListSecretVersionsResponse(proto.Message): diff --git a/samples/snippets/consume_event_notification.py b/samples/snippets/consume_event_notification.py new file mode 100644 index 0000000..dcc9f73 --- /dev/null +++ b/samples/snippets/consume_event_notification.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# Copyright 2021 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 +""" +sample code for consuming an event notification in a cloud function. +""" + +import base64 + + +# [START secretmanager_consume_event_notification] +def consume_event_notification(event, unused_context): + """ + consume_event_notification demonstrates how to consume and process a + Pub/Sub notification from Secret Manager. + Args: + event (dict): Event payload. + unused_context (google.cloud.functions.Context): Metadata for the event. + """ + event_type = event['attributes']['eventType'] + secret_id = event['attributes']['secretId'] + secret_metadata = base64.b64decode(event['data']).decode('utf-8') + return f'Received {event_type} for {secret_id}. New metadata: {secret_metadata}' +# [END secretmanager_consume_event_notification] diff --git a/samples/snippets/delete_secret_with_etag.py b/samples/snippets/delete_secret_with_etag.py new file mode 100644 index 0000000..063ffea --- /dev/null +++ b/samples/snippets/delete_secret_with_etag.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +# Copyright 2021 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 deleting an existing secret. +""" + +import argparse + + +# [START secretmanager_delete_secret_with_etag] +def delete_secret_with_etag(project_id, secret_id, etag): + """ + Delete the secret with the given name, etag, and all of its versions. + """ + + # Import the Secret Manager client library and types. + from google.cloud import secretmanager + from google.cloud.secretmanager_v1.types import service + + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret. + name = client.secret_path(project_id, secret_id) + + # Build the request + request = service.DeleteSecretRequest() + request.name = name + request.etag = etag + + # Delete the secret. + client.delete_secret(request=request) + + +# [END secretmanager_delete_secret_with_etag] + + +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 delete") + parser.add_argument("etag", help="current etag of the secret to delete") + args = parser.parse_args() + + delete_secret_with_etag(args.project_id, args.secret_id, args.etag) diff --git a/samples/snippets/destroy_secret_version_with_etag.py b/samples/snippets/destroy_secret_version_with_etag.py new file mode 100644 index 0000000..b7512e3 --- /dev/null +++ b/samples/snippets/destroy_secret_version_with_etag.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# Copyright 2021 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 destroying a secret verison. +""" + +import argparse + + +# [START secretmanager_destroy_secret_version_with_etag] +def destroy_secret_version_with_etag(project_id, secret_id, version_id, etag): + """ + Destroy the given secret version, making the payload irrecoverable. Other + secrets versions are unaffected. + """ + + # Import the Secret Manager client library. + from google.cloud import secretmanager + from google.cloud.secretmanager_v1.types import service + + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret version + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + # Build the request + request = service.DestroySecretVersionRequest() + request.name = name + request.etag = etag + + # Destroy the secret version. + response = client.destroy_secret_version(request=request) + + print("Destroyed secret version: {}".format(response.name)) + # [END secretmanager_destroy_secret_version_with_etag] + + return response + + +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 from which to act") + parser.add_argument("version_id", help="id of the version to destroy") + parser.add_argument("etag", help="current etag of the version") + args = parser.parse_args() + + destroy_secret_version_with_etag( + args.project_id, args.secret_id, args.version_id, args.etag) diff --git a/samples/snippets/disable_secret_version_with_etag.py b/samples/snippets/disable_secret_version_with_etag.py new file mode 100644 index 0000000..6b1287b --- /dev/null +++ b/samples/snippets/disable_secret_version_with_etag.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# Copyright 2021 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 disabling a secret version. +""" + +import argparse + + +# [START secretmanager_disable_secret_version_with_etag] +def disable_secret_version_with_etag(project_id, secret_id, version_id, etag): + """ + Disable the given secret version. Future requests will throw an error until + the secret version is enabled. Other secrets versions are unaffected. + """ + + # Import the Secret Manager client library. + from google.cloud import secretmanager + from google.cloud.secretmanager_v1.types import service + + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret version + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + # Build the request + request = service.DisableSecretVersionRequest() + request.name = name + request.etag = etag + + # Disable the secret version. + response = client.disable_secret_version(request=request) + + print("Disabled secret version: {}".format(response.name)) + # [END secretmanager_disable_secret_version_with_etag] + + return response + + +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 from which to act") + parser.add_argument("version_id", help="id of the version to disable") + parser.add_argument("etag", help="current etag of the version") + args = parser.parse_args() + + disable_secret_version_with_etag( + args.project_id, args.secret_id, args.version_id, args.etag) diff --git a/samples/snippets/enable_secret_version_with_etag.py b/samples/snippets/enable_secret_version_with_etag.py new file mode 100644 index 0000000..7b7e3d7 --- /dev/null +++ b/samples/snippets/enable_secret_version_with_etag.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# Copyright 2021 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 enabling a secret version. +""" + +import argparse + + +# [START secretmanager_enable_secret_version_with_etag] +def enable_secret_version_with_etag(project_id, secret_id, version_id, etag): + """ + Enable the given secret version, enabling it to be accessed after + previously being disabled. Other secrets versions are unaffected. + """ + + # Import the Secret Manager client library. + from google.cloud import secretmanager + from google.cloud.secretmanager_v1.types import service + + # Create the Secret Manager client. + client = secretmanager.SecretManagerServiceClient() + + # Build the resource name of the secret version + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + # Build the request + request = service.EnableSecretVersionRequest() + request.name = name + request.etag = etag + + # Disable the secret version. + response = client.enable_secret_version(request=request) + + print("Enabled secret version: {}".format(response.name)) + # [END secretmanager_enable_secret_version_with_etag] + + return response + + +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 from which to act") + parser.add_argument("version_id", help="id of the version to enable") + parser.add_argument("etag", help="current etag of the version") + args = parser.parse_args() + + enable_secret_version_with_etag( + args.project_id, args.secret_id, args.version_id, args.etag) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 5ff9e1d..6a8ccda 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -28,8 +28,9 @@ # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING -# Copy `noxfile_config.py` to your directory and modify it instead. +BLACK_VERSION = "black==19.10b0" +# Copy `noxfile_config.py` to your directory and modify it instead. # `TEST_CONFIG` dict is a configuration hook that allows users to # modify the test configurations. The values here should be in sync @@ -159,7 +160,7 @@ def lint(session: nox.sessions.Session) -> None: @nox.session def blacken(session: nox.sessions.Session) -> None: - session.install("black") + session.install(BLACK_VERSION) python_files = [path for path in os.listdir(".") if path.endswith(".py")] session.run("black", *python_files) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 944cbe4..a94e20d 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1 +1 @@ -google-cloud-secret-manager==2.5.0 \ No newline at end of file +google-cloud-secret-manager==2.6.0 \ No newline at end of file diff --git a/samples/snippets/snippets_test.py b/samples/snippets/snippets_test.py index d48d306..29475cf 100644 --- a/samples/snippets/snippets_test.py +++ b/samples/snippets/snippets_test.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and +import base64 import os import uuid @@ -20,11 +21,16 @@ from access_secret_version import access_secret_version from add_secret_version import add_secret_version +from consume_event_notification import consume_event_notification from create_secret import create_secret from delete_secret import delete_secret +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 disable_secret_version import disable_secret_version +from disable_secret_version_with_etag import disable_secret_version_with_etag from enable_secret_version import enable_secret_version +from enable_secret_version_with_etag import enable_secret_version_with_etag from get_secret import get_secret from get_secret_version import get_secret_version from iam_grant_access import iam_grant_access @@ -33,6 +39,7 @@ from list_secrets import list_secrets from quickstart import quickstart from update_secret import update_secret +from update_secret_with_etag import update_secret_with_etag @pytest.fixture() @@ -64,7 +71,7 @@ def secret(client, project_id): } ) - yield project_id, secret_id + yield project_id, secret_id, secret.etag print("deleting secret {}".format(secret_id)) try: @@ -79,7 +86,7 @@ def secret(client, project_id): @pytest.fixture() def secret_version(client, secret): - project_id, secret_id = secret + project_id, secret_id, _ = secret print("adding secret version to {}".format(secret_id)) parent = client.secret_path(project_id, secret_id) @@ -88,25 +95,36 @@ def secret_version(client, secret): request={"parent": parent, "payload": {"data": payload}} ) - yield project_id, secret_id, version.name.rsplit("/", 1)[-1] + yield project_id, secret_id, version.name.rsplit("/", 1)[-1], version.etag another_secret_version = secret_version +@pytest.fixture() +def pubsub_message(): + message = "hello!" + message_bytes = message.encode() + base64_bytes = base64.b64encode(message_bytes) + return { + "attributes": {"eventType": "SECRET_UPDATE", "secretId": "projects/p/secrets/s"}, + "data": base64_bytes + } + + def test_quickstart(project_id): secret_id = "python-secret-{}".format(uuid.uuid4()) quickstart(project_id, secret_id) def test_access_secret_version(secret_version): - project_id, secret_id, version_id = secret_version + project_id, secret_id, version_id, _ = secret_version version = access_secret_version(project_id, secret_id, version_id) assert version.payload.data == b"hello world!" def test_add_secret_version(secret): - project_id, secret_id = secret + project_id, secret_id, _ = secret payload = "test123" version = add_secret_version(project_id, secret_id, payload) assert secret_id in version.name @@ -120,7 +138,7 @@ def test_create_secret(client, project_id): def test_delete_secret(client, secret): - project_id, secret_id = secret + project_id, secret_id, _ = secret delete_secret(project_id, secret_id) with pytest.raises(exceptions.NotFound): print("{}".format(client)) @@ -128,14 +146,29 @@ def test_delete_secret(client, secret): client.access_secret_version(request={"name": name}) +def test_delete_secret_with_etag(client, secret): + project_id, secret_id, etag = secret + delete_secret_with_etag(project_id, secret_id, etag) + with pytest.raises(exceptions.NotFound): + print("{}".format(client)) + name = f"projects/{project_id}/secrets/{secret_id}/versions/latest" + client.access_secret_version(request={"name": name}) + + def test_destroy_secret_version(client, secret_version): - project_id, secret_id, version_id = secret_version + project_id, secret_id, version_id, _ = secret_version version = destroy_secret_version(project_id, secret_id, version_id) assert version.destroy_time +def test_destroy_secret_version_with_etag(client, secret_version): + project_id, secret_id, version_id, etag = secret_version + version = destroy_secret_version_with_etag(project_id, secret_id, version_id, etag) + assert version.destroy_time + + def test_enable_disable_secret_version(client, secret_version): - project_id, secret_id, version_id = secret_version + project_id, secret_id, version_id, _ = secret_version version = disable_secret_version(project_id, secret_id, version_id) assert version.state == secretmanager.SecretVersion.State.DISABLED @@ -143,34 +176,43 @@ def test_enable_disable_secret_version(client, secret_version): assert version.state == secretmanager.SecretVersion.State.ENABLED +def test_enable_disable_secret_version_with_etag(client, secret_version): + project_id, secret_id, version_id, etag = secret_version + version = disable_secret_version_with_etag(project_id, secret_id, version_id, etag) + assert version.state == secretmanager.SecretVersion.State.DISABLED + + version = enable_secret_version_with_etag(project_id, secret_id, version_id, version.etag) + assert version.state == secretmanager.SecretVersion.State.ENABLED + + def test_get_secret_version(client, secret_version): - project_id, secret_id, version_id = secret_version + project_id, secret_id, version_id, _ = secret_version version = get_secret_version(project_id, secret_id, version_id) assert secret_id in version.name assert version_id in version.name def test_get_secret(client, secret): - project_id, secret_id = secret + project_id, secret_id, _ = secret snippet_secret = get_secret(project_id, secret_id) assert secret_id in snippet_secret.name def test_iam_grant_access(client, secret, iam_user): - project_id, secret_id = secret + project_id, secret_id, _ = secret policy = iam_grant_access(project_id, secret_id, iam_user) assert any(iam_user in b.members for b in policy.bindings) def test_iam_revoke_access(client, secret, iam_user): - project_id, secret_id = secret + project_id, secret_id, _ = secret policy = iam_revoke_access(project_id, secret_id, iam_user) assert not any(iam_user in b.members for b in policy.bindings) def test_list_secret_versions(capsys, secret_version, another_secret_version): - project_id, secret_id, version_id = secret_version - _, _, another_version_id = another_secret_version + project_id, secret_id, version_id, _ = secret_version + _, _, another_version_id, _ = another_secret_version list_secret_versions(project_id, secret_id) out, _ = capsys.readouterr() @@ -180,8 +222,8 @@ def test_list_secret_versions(capsys, secret_version, another_secret_version): def test_list_secrets(capsys, secret, another_secret): - project_id, secret_id = secret - _, another_secret_id = another_secret + project_id, secret_id, _ = secret + _, another_secret_id, _ = another_secret list_secrets(project_id) out, _ = capsys.readouterr() @@ -190,6 +232,17 @@ def test_list_secrets(capsys, secret, another_secret): def test_update_secret(secret): - project_id, secret_id = secret + project_id, secret_id, _ = secret secret = update_secret(project_id, secret_id) assert secret.labels["secretmanager"] == "rocks" + + +def test_consume_event_notification(pubsub_message): + got = consume_event_notification(pubsub_message, None) + assert got == "Received SECRET_UPDATE for projects/p/secrets/s. New metadata: hello!" + + +def test_update_secret_with_etag(secret): + project_id, secret_id, etag = secret + secret = update_secret_with_etag(project_id, secret_id, etag) + assert secret.labels["secretmanager"] == "rocks" diff --git a/samples/snippets/update_secret_with_etag.py b/samples/snippets/update_secret_with_etag.py new file mode 100644 index 0000000..772ea8c --- /dev/null +++ b/samples/snippets/update_secret_with_etag.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# Copyright 2021 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 + +import argparse + + +# [START secretmanager_update_secret_with_etag] +def update_secret_with_etag(project_id, secret_id, etag): + """ + Update the metadata about an existing secret, using etag. + """ + + # Import the Secret Manager client library. + from google.cloud import secretmanager + + # 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 secret. + secret = {"name": name, "labels": {"secretmanager": "rocks"}, "etag": etag} + update_mask = {"paths": ["labels"]} + response = client.update_secret( + request={"secret": secret, "update_mask": update_mask} + ) + + # Print the new secret name. + print("Updated secret: {}".format(response.name)) + # [END secretmanager_update_secret_with_etag] + + return response + + +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", required=True) + parser.add_argument("etag", help="current etag of the secret") + args = parser.parse_args() + + update_secret_with_etag(args.project_id, args.secret_id, args.etag) diff --git a/scripts/fixup_secretmanager_v1_keywords.py b/scripts/fixup_secretmanager_v1_keywords.py index 6f922e8..e8e12e1 100644 --- a/scripts/fixup_secretmanager_v1_keywords.py +++ b/scripts/fixup_secretmanager_v1_keywords.py @@ -49,8 +49,8 @@ class secretmanagerCallTransformer(cst.CSTTransformer): 'get_iam_policy': ('resource', 'options', ), 'get_secret': ('name', ), 'get_secret_version': ('name', ), - 'list_secrets': ('parent', 'page_size', 'page_token', ), - 'list_secret_versions': ('parent', 'page_size', 'page_token', ), + 'list_secrets': ('parent', 'page_size', 'page_token', 'filter', ), + 'list_secret_versions': ('parent', 'page_size', 'page_token', 'filter', ), 'set_iam_policy': ('resource', 'policy', ), 'test_iam_permissions': ('resource', 'permissions', ), 'update_secret': ('secret', 'update_mask', ), diff --git a/setup.py b/setup.py index 2aeaebd..0c6a46c 100644 --- a/setup.py +++ b/setup.py @@ -22,10 +22,13 @@ name = "google-cloud-secret-manager" description = "Secret Manager API API client library" -version = "2.6.0" +version = "2.7.0" release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 1.26.0, <2.0.0dev", + # NOTE: Maintainers, please do not require google-api-core>=2.x.x + # Until this issue is closed + # https://github.com/googleapis/google-cloud-python/issues/10566 + "google-api-core[grpc] >= 1.26.0, <3.0.0dev", "grpc-google-iam-v1 >= 0.12.3, < 0.13dev", "proto-plus >= 1.4.0", "packaging >= 14.3", diff --git a/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 94d2aa4..1195d7f 100644 --- a/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -131,18 +131,6 @@ def test_secret_manager_service_client_from_service_account_info(client_class): assert client.transport._host == "secretmanager.googleapis.com:443" -@pytest.mark.parametrize( - "client_class", [SecretManagerServiceClient, SecretManagerServiceAsyncClient,] -) -def test_secret_manager_service_client_service_account_always_use_jwt(client_class): - with mock.patch.object( - service_account.Credentials, "with_always_use_jwt_access", create=True - ) as use_jwt: - creds = service_account.Credentials(None, None, None) - client = client_class(credentials=creds) - use_jwt.assert_not_called() - - @pytest.mark.parametrize( "transport_class,transport_name", [ @@ -150,7 +138,7 @@ def test_secret_manager_service_client_service_account_always_use_jwt(client_cla (transports.SecretManagerServiceGrpcAsyncIOTransport, "grpc_asyncio"), ], ) -def test_secret_manager_service_client_service_account_always_use_jwt_true( +def test_secret_manager_service_client_service_account_always_use_jwt( transport_class, transport_name ): with mock.patch.object( @@ -160,6 +148,13 @@ def test_secret_manager_service_client_service_account_always_use_jwt_true( transport = transport_class(credentials=creds, always_use_jwt_access=True) use_jwt.assert_called_once_with(True) + with mock.patch.object( + service_account.Credentials, "with_always_use_jwt_access", create=True + ) as use_jwt: + creds = service_account.Credentials(None, None, None) + transport = transport_class(credentials=creds, always_use_jwt_access=False) + use_jwt.assert_not_called() + @pytest.mark.parametrize( "client_class", [SecretManagerServiceClient, SecretManagerServiceAsyncClient,] @@ -244,6 +239,7 @@ def test_secret_manager_service_client_client_options( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is @@ -260,6 +256,7 @@ def test_secret_manager_service_client_client_options( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is @@ -276,6 +273,7 @@ def test_secret_manager_service_client_client_options( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT has @@ -304,6 +302,7 @@ def test_secret_manager_service_client_client_options( client_cert_source_for_mtls=None, quota_project_id="octopus", client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) @@ -380,6 +379,7 @@ def test_secret_manager_service_client_mtls_env_auto( client_cert_source_for_mtls=expected_client_cert_source, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) # Check the case ADC client cert is provided. Whether client cert is used depends on @@ -413,6 +413,7 @@ def test_secret_manager_service_client_mtls_env_auto( client_cert_source_for_mtls=expected_client_cert_source, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) # Check the case client_cert_source and ADC client cert are not provided. @@ -434,6 +435,7 @@ def test_secret_manager_service_client_mtls_env_auto( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) @@ -468,6 +470,7 @@ def test_secret_manager_service_client_client_options_scopes( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) @@ -502,6 +505,7 @@ def test_secret_manager_service_client_client_options_credentials_file( client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, ) @@ -521,6 +525,7 @@ def test_secret_manager_service_client_client_options_from_dict(): client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, )