From 110452375c6ccaa16e4ade7d7fe3438d185d4355 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 1 Apr 2026 19:04:44 -0700 Subject: [PATCH] refactor: Move SecretManagerClient to google.adk.integrations.secret_manager package Reasons for moving `SecretManagerClient`: 1. **Logical Grouping**: `SecretManagerClient` represents an integration with Google Cloud Secret Manager. Placing it in the `integrations` package is more intuitive and organized than keeping it within a specific tool's client directory. 2. **Increased Reusability**: As a common utility in `google.adk.integrations`, it is now easily discoverable and accessible for any other agent or tool in the ADK that needs to interact with Google Cloud Secret Manager. 3. **Better Abstraction**: It provides a clean, simplified interface for secret retrieval from Google Cloud Secret Manager. Future enhancements will be consolidated in the same package. 4. **Cleaner Tooling**: The `apihub_tool` can now focus purely on the API Hub logic while delegating secret management to this dedicated package. For what its worth, `SecretManagerClient` is not used by the `apihub_tool` at the moment. PiperOrigin-RevId: 893221472 --- .../integrations/secret_manager/__init__.py | 19 +++ .../secret_manager/secret_client.py | 121 ++++++++++++++++++ .../apihub_tool/clients/secret_client.py | 117 ++--------------- .../integrations/secret_manager/__init__.py | 13 ++ .../secret_manager}/test_secret_client.py | 10 +- .../clients/test_secret_client_deprecated.py | 33 +++++ 6 files changed, 204 insertions(+), 109 deletions(-) create mode 100644 src/google/adk/integrations/secret_manager/__init__.py create mode 100644 src/google/adk/integrations/secret_manager/secret_client.py create mode 100644 tests/unittests/integrations/secret_manager/__init__.py rename tests/unittests/{tools/apihub_tool/clients => integrations/secret_manager}/test_secret_client.py (93%) create mode 100644 tests/unittests/tools/apihub_tool/clients/test_secret_client_deprecated.py diff --git a/src/google/adk/integrations/secret_manager/__init__.py b/src/google/adk/integrations/secret_manager/__init__.py new file mode 100644 index 0000000000..9c1dbd53bd --- /dev/null +++ b/src/google/adk/integrations/secret_manager/__init__.py @@ -0,0 +1,19 @@ +# 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. + +from .secret_client import SecretManagerClient + +__all__ = [ + 'SecretManagerClient', +] diff --git a/src/google/adk/integrations/secret_manager/secret_client.py b/src/google/adk/integrations/secret_manager/secret_client.py new file mode 100644 index 0000000000..7f76ff3712 --- /dev/null +++ b/src/google/adk/integrations/secret_manager/secret_client.py @@ -0,0 +1,121 @@ +# 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. + +from __future__ import annotations + +import json +from typing import cast +from typing import Optional + +import google.auth +from google.auth import default as default_service_credential +import google.auth.transport.requests +from google.cloud import secretmanager +from google.oauth2 import service_account + + +class SecretManagerClient: + """A client for interacting with Google Cloud Secret Manager. + + This class provides a simplified interface for retrieving secrets from + Secret Manager, handling authentication using either a service account + JSON keyfile (passed as a string) or a preexisting authorization token. + + Attributes: + _credentials: Google Cloud credentials object (ServiceAccountCredentials + or Credentials). + _client: Secret Manager client instance. + """ + + def __init__( + self, + service_account_json: Optional[str] = None, + auth_token: Optional[str] = None, + ): + """Initializes the SecretManagerClient. + + Args: + service_account_json: The content of a service account JSON keyfile (as + a string), not the file path. Must be valid JSON. + auth_token: An existing Google Cloud authorization token. + + Raises: + ValueError: If neither `service_account_json` nor `auth_token` is + provided, + or if both are provided. Also raised if the service_account_json + is not valid JSON. + google.auth.exceptions.GoogleAuthError: If authentication fails. + """ + if service_account_json: + try: + credentials = service_account.Credentials.from_service_account_info( + json.loads(service_account_json) + ) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid service account JSON: {e}") from e + elif auth_token: + credentials = google.auth.credentials.Credentials( + token=auth_token, + refresh_token=None, + token_uri=None, + client_id=None, + client_secret=None, + ) + request = google.auth.transport.requests.Request() + credentials.refresh(request) + else: + try: + credentials, _ = default_service_credential( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + except Exception as e: + raise ValueError( + "'service_account_json' or 'auth_token' are both missing, and" + f" error occurred while trying to use default credentials: {e}" + ) from e + + if not credentials: + raise ValueError( + "Must provide either 'service_account_json' or 'auth_token', not both" + " or neither." + ) + + self._credentials = credentials + self._client = secretmanager.SecretManagerServiceClient( + credentials=self._credentials + ) + + def get_secret(self, resource_name: str) -> str: + """Retrieves a secret from Google Cloud Secret Manager. + + Args: + resource_name: The full resource name of the secret, in the format + "projects/*/secrets/*/versions/*". Usually you want the "latest" + version, e.g., + "projects/my-project/secrets/my-secret/versions/latest". + + Returns: + The secret payload as a string. + + Raises: + google.api_core.exceptions.GoogleAPIError: If the Secret Manager API + returns an error (e.g., secret not found, permission denied). + Exception: For other unexpected errors. + """ + try: + response = self._client.access_secret_version(name=resource_name) + return cast(str, response.payload.data.decode("UTF-8")) + except Exception as e: + raise e # Re-raise the exception to allow for handling by the caller + # Consider logging the exception here before re-raising. diff --git a/src/google/adk/tools/apihub_tool/clients/secret_client.py b/src/google/adk/tools/apihub_tool/clients/secret_client.py index 2d83fe7cb5..a7d0079e16 100644 --- a/src/google/adk/tools/apihub_tool/clients/secret_client.py +++ b/src/google/adk/tools/apihub_tool/clients/secret_client.py @@ -14,107 +14,16 @@ from __future__ import annotations -import json -from typing import Optional - -import google.auth -from google.auth import default as default_service_credential -import google.auth.transport.requests -from google.cloud import secretmanager -from google.oauth2 import service_account - - -class SecretManagerClient: - """A client for interacting with Google Cloud Secret Manager. - - This class provides a simplified interface for retrieving secrets from - Secret Manager, handling authentication using either a service account - JSON keyfile (passed as a string) or a preexisting authorization token. - - Attributes: - _credentials: Google Cloud credentials object (ServiceAccountCredentials - or Credentials). - _client: Secret Manager client instance. - """ - - def __init__( - self, - service_account_json: Optional[str] = None, - auth_token: Optional[str] = None, - ): - """Initializes the SecretManagerClient. - - Args: - service_account_json: The content of a service account JSON keyfile (as - a string), not the file path. Must be valid JSON. - auth_token: An existing Google Cloud authorization token. - - Raises: - ValueError: If neither `service_account_json` nor `auth_token` is - provided, - or if both are provided. Also raised if the service_account_json - is not valid JSON. - google.auth.exceptions.GoogleAuthError: If authentication fails. - """ - if service_account_json: - try: - credentials = service_account.Credentials.from_service_account_info( - json.loads(service_account_json) - ) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid service account JSON: {e}") from e - elif auth_token: - credentials = google.auth.credentials.Credentials( - token=auth_token, - refresh_token=None, - token_uri=None, - client_id=None, - client_secret=None, - ) - request = google.auth.transport.requests.Request() - credentials.refresh(request) - else: - try: - credentials, _ = default_service_credential( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) - except Exception as e: - raise ValueError( - "'service_account_json' or 'auth_token' are both missing, and" - f" error occurred while trying to use default credentials: {e}" - ) from e - - if not credentials: - raise ValueError( - "Must provide either 'service_account_json' or 'auth_token', not both" - " or neither." - ) - - self._credentials = credentials - self._client = secretmanager.SecretManagerServiceClient( - credentials=self._credentials - ) - - def get_secret(self, resource_name: str) -> str: - """Retrieves a secret from Google Cloud Secret Manager. - - Args: - resource_name: The full resource name of the secret, in the format - "projects/*/secrets/*/versions/*". Usually you want the "latest" - version, e.g., - "projects/my-project/secrets/my-secret/versions/latest". - - Returns: - The secret payload as a string. - - Raises: - google.api_core.exceptions.GoogleAPIError: If the Secret Manager API - returns an error (e.g., secret not found, permission denied). - Exception: For other unexpected errors. - """ - try: - response = self._client.access_secret_version(name=resource_name) - return response.payload.data.decode("UTF-8") - except Exception as e: - raise e # Re-raise the exception to allow for handling by the caller - # Consider logging the exception here before re-raising. +import warnings + +try: + from google.adk.integrations.secret_manager.secret_client import SecretManagerClient + + warnings.warn( + "SecretManagerClient has been moved to" + " google.adk.integrations.secret_manager. Please update your imports.", + DeprecationWarning, + stacklevel=2, + ) +except ImportError: + pass diff --git a/tests/unittests/integrations/secret_manager/__init__.py b/tests/unittests/integrations/secret_manager/__init__.py new file mode 100644 index 0000000000..58d482ea38 --- /dev/null +++ b/tests/unittests/integrations/secret_manager/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/unittests/tools/apihub_tool/clients/test_secret_client.py b/tests/unittests/integrations/secret_manager/test_secret_client.py similarity index 93% rename from tests/unittests/tools/apihub_tool/clients/test_secret_client.py rename to tests/unittests/integrations/secret_manager/test_secret_client.py index 4f18a6c8f9..1f11a22c0e 100644 --- a/tests/unittests/tools/apihub_tool/clients/test_secret_client.py +++ b/tests/unittests/integrations/secret_manager/test_secret_client.py @@ -18,7 +18,7 @@ from unittest.mock import MagicMock from unittest.mock import patch -from google.adk.tools.apihub_tool.clients.secret_client import SecretManagerClient +from google.adk.integrations.secret_manager.secret_client import SecretManagerClient import pytest import google @@ -29,7 +29,7 @@ class TestSecretManagerClient: @patch("google.cloud.secretmanager.SecretManagerServiceClient") @patch( - "google.adk.tools.apihub_tool.clients.secret_client.default_service_credential" + "google.adk.integrations.secret_manager.secret_client.default_service_credential" ) def test_init_with_default_credentials( self, mock_default_service_credential, mock_secret_manager_client @@ -112,7 +112,7 @@ def test_init_with_auth_token(self, mock_secret_manager_client): assert client._client == mock_secret_manager_client.return_value @patch( - "google.adk.tools.apihub_tool.clients.secret_client.default_service_credential" + "google.adk.integrations.secret_manager.secret_client.default_service_credential" ) def test_init_with_default_credentials_error( self, mock_default_service_credential @@ -136,7 +136,7 @@ def test_init_with_invalid_service_account_json(self): @patch("google.cloud.secretmanager.SecretManagerServiceClient") @patch( - "google.adk.tools.apihub_tool.clients.secret_client.default_service_credential" + "google.adk.integrations.secret_manager.secret_client.default_service_credential" ) def test_get_secret( self, mock_default_service_credential, mock_secret_manager_client @@ -170,7 +170,7 @@ def test_get_secret( @patch("google.cloud.secretmanager.SecretManagerServiceClient") @patch( - "google.adk.tools.apihub_tool.clients.secret_client.default_service_credential" + "google.adk.integrations.secret_manager.secret_client.default_service_credential" ) def test_get_secret_error( self, mock_default_service_credential, mock_secret_manager_client diff --git a/tests/unittests/tools/apihub_tool/clients/test_secret_client_deprecated.py b/tests/unittests/tools/apihub_tool/clients/test_secret_client_deprecated.py new file mode 100644 index 0000000000..cc933624b7 --- /dev/null +++ b/tests/unittests/tools/apihub_tool/clients/test_secret_client_deprecated.py @@ -0,0 +1,33 @@ +# 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 sys +import warnings + +from google.adk.integrations.secret_manager import secret_client +import pytest + + +def test_secret_client_module_deprecation(): + """Verifies that importing from clients.secret_client triggers a warning.""" + module_to_test = "google.adk.tools.apihub_tool.clients.secret_client" + if module_to_test in sys.modules: + sys.modules.pop(module_to_test) + + with pytest.warns( + DeprecationWarning, match="google.adk.integrations.secret_manager" + ): + from google.adk.tools.apihub_tool.clients.secret_client import SecretManagerClient as deprecated_secret_client + + assert deprecated_secret_client is secret_client.SecretManagerClient