Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,16 @@
)

import google.protobuf
from google.api_core import _observability, gapic_v1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When actually implementing this, we shoudl probably guard this, so we can fail gracefully, right? (IIRC, the plan wasn't to force an upgrade for this feature?)

try:
   from google.api_core import _observability
catch ImportError:
  _observability = None

from google.api_core import client_options as client_options_lib
from google.api_core import exceptions as core_exceptions
from google.api_core import gapic_v1
from google.api_core import retry as retries
from google.auth import credentials as ga_credentials # type: ignore
from google.auth.exceptions import MutualTLSChannelError # type: ignore
from google.auth.transport import mtls # type: ignore
from google.auth.transport.grpc import SslCredentials # type: ignore
from google.oauth2 import service_account # type: ignore

from google.cloud.secretmanager_v1 import gapic_version as package_version
from google.oauth2 import service_account # type: ignore

try:
OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None]
Expand All @@ -68,7 +67,6 @@
import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore
import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore
from google.cloud.location import locations_pb2 # type: ignore

from google.cloud.secretmanager_v1.services.secret_manager_service import pagers
from google.cloud.secretmanager_v1.types import resources, service

Expand Down Expand Up @@ -746,17 +744,28 @@ def __init__(
else cast(Callable[..., SecretManagerServiceTransport], transport)
)
# initialize with the provided callable or the passed in class
self._transport = transport_init(
credentials=credentials,
credentials_file=self._client_options.credentials_file,
host=self._api_endpoint,
scopes=self._client_options.scopes,
client_cert_source_for_mtls=self._client_cert_source,
quota_project_id=self._client_options.quota_project_id,
client_info=client_info,
always_use_jwt_access=True,
api_audience=self._client_options.api_audience,
)
transport_kwargs = {
"credentials": credentials,
"credentials_file": self._client_options.credentials_file,
"host": self._api_endpoint,
"scopes": self._client_options.scopes,
"client_cert_source_for_mtls": self._client_cert_source,
"quota_project_id": self._client_options.quota_project_id,
"client_info": client_info,
"always_use_jwt_access": True,
"api_audience": self._client_options.api_audience,
}

# When OpenTelemetry tracing is enabled, obtain the channel interceptor
# and pass it to the transport.
if transport_init is SecretManagerServiceGrpcTransport:
otel_interceptor = _observability.get_otel_interceptor(
self._client_options
)
if otel_interceptor is not None:
transport_kwargs["interceptors"] = [otel_interceptor]

self._transport = transport_init(**transport_kwargs)

if "async" not in str(self._transport):
if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
import grpc # type: ignore
import proto # type: ignore
from google.api_core import gapic_v1, grpc_helpers
from google.api_core.grpc_helpers import ClientInterceptor
from google.auth import credentials as ga_credentials # type: ignore
from google.auth.transport.grpc import SslCredentials # type: ignore
from google.cloud.location import locations_pb2 # type: ignore
from google.protobuf.json_format import MessageToJson

from google.cloud.secretmanager_v1.types import resources, service
from google.protobuf.json_format import MessageToJson

from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport

Expand Down Expand Up @@ -148,6 +148,14 @@ def __init__(
client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO,
always_use_jwt_access: Optional[bool] = False,
api_audience: Optional[str] = None,
interceptors: Optional[
Sequence[
Union[
ClientInterceptor,
Callable[[grpc.Channel], grpc.Channel],
]
]
] = None,
) -> None:
"""Instantiate the transport.

Expand Down Expand Up @@ -198,6 +206,9 @@ def __init__(
to the service that will be set when using certain 3rd party
authentication flows. Audience is typically a resource identifier.
If not set, the host value will be used as a default.
interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]):
Additional interceptors (or callables that apply interceptors) to apply to the
gRPC channel.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying implementation is looking good

Regarding the naming: I think we should still stick with the standard interceptors name here. It seems like you're trying to make a distinction between channel wrappers and interceptors, but I don't think there's a meaningful distinction there. Interceptor is essentially just the name gRPC uses for a channel wrapper, so we should stick with standard gRPC terminology rather than inventing new types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the docstring, could we do something like this?

interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]):
                Additional interceptors (or callables that apply interceptors) to apply to the gRPC channel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Raises:
google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport
Expand Down Expand Up @@ -274,6 +285,10 @@ def __init__(
],
)

self._grpc_channel = grpc_helpers.apply_channel_interceptors(
self._grpc_channel, interceptors
)

self._interceptor = _LoggingClientInterceptor()
self._logged_channel = grpc.intercept_channel(
self._grpc_channel, self._interceptor
Expand Down
4 changes: 3 additions & 1 deletion packages/google-cloud-secret-manager/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
"pytest-asyncio",
]
UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = []
UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = []
UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [
"../google-api-core[tracing,testing]",
]
UNIT_TEST_DEPENDENCIES: List[str] = []
UNIT_TEST_EXTRAS: List[str] = []
UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {}
Expand Down
2 changes: 1 addition & 1 deletion packages/google-cloud-secret-manager/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
release_status = "Development Status :: 5 - Production/Stable"

dependencies = [
"google-api-core[grpc] >= 2.25.0, <3.0.0",
"google-api-core[grpc] >= 2.35.0, <3.0.0",
# Exclude incompatible versions of `google-auth`
# See https://github.com/googleapis/google-cloud-python/issues/12364
"google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# pinning their versions to their lower bounds.
# For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0",
# then this file should have google-cloud-foo==1.14.0
google-api-core==2.25.0
google-api-core==2.35.0
google-auth==2.14.1
grpcio==1.59.0
proto-plus==1.26.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.
#
import asyncio
import functools
import json
import math
import os
Expand Down Expand Up @@ -61,15 +62,14 @@
from google.auth import credentials as ga_credentials
from google.auth.exceptions import MutualTLSChannelError
from google.cloud.location import locations_pb2
from google.oauth2 import service_account

from google.cloud.secretmanager_v1.services.secret_manager_service import (
SecretManagerServiceAsyncClient,
SecretManagerServiceClient,
pagers,
transports,
)
from google.cloud.secretmanager_v1.types import resources, service
from google.oauth2 import service_account

CRED_INFO_JSON = {
"credential_source": "/path/to/file",
Expand Down Expand Up @@ -770,6 +770,103 @@ def test_secret_manager_service_client_client_options(
)


def test_secret_manager_service_client_otel_channel_injection_enabled():
"""Proves that when OpenTelemetry tracing is enabled:

1. SecretManagerServiceClient obtains the channel interceptor via
_observability.get_otel_interceptor passing client_options.
2. The interceptor is passed into transport kwargs under 'interceptors',
allowing the Transport to apply it via apply_channel_interceptors.
"""
mock_interceptor = mock.Mock()
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor",
return_value=mock_interceptor,
) as mock_get_interceptor,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")

mock_get_interceptor.assert_called_once_with(client._client_options)
called_kwargs = patched_transport_init.call_args.kwargs
assert "interceptors" in called_kwargs
assert called_kwargs["interceptors"] == [mock_interceptor]


def test_secret_manager_service_client_otel_channel_injection_disabled():
"""Proves that when OpenTelemetry tracing is disabled:

1. SecretManagerServiceClient checks for an OTel interceptor and receives None.
2. No 'interceptors' argument is passed to the transport constructor.
"""
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor",
return_value=None,
) as mock_get_interceptor,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")

mock_get_interceptor.assert_called_once_with(client._client_options)
called_kwargs = patched_transport_init.call_args.kwargs
assert "interceptors" not in called_kwargs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should pass in an empty list, and assert that the otel inteceptor isn't present? This test will soon be out of date, since there will be other default interceptors



def test_secret_manager_service_grpc_transport_interceptors():
"""Proves that SecretManagerServiceGrpcTransport accepts channel interceptors
and invokes grpc_helpers.apply_channel_interceptors to apply them to the underlying
gRPC channel.
"""
mock_interceptor = mock.Mock()
mock_channel = mock.Mock()

with (
mock.patch.object(
transports.SecretManagerServiceGrpcTransport,
"create_channel",
return_value=mock_channel,
),
mock.patch(
"google.api_core.grpc_helpers.apply_channel_interceptors",
return_value=mock_channel,
) as mock_apply_interceptors,
):
transports.SecretManagerServiceGrpcTransport(
interceptors=[mock_interceptor],
)

mock_apply_interceptors.assert_called_once_with(
mock_channel, [mock_interceptor]
)


def test_secret_manager_service_grpc_transport_custom_channel_interceptors():
"""Proves that SecretManagerServiceGrpcTransport wraps explicitly passed custom channels
using grpc_helpers.apply_channel_interceptors.
"""
mock_interceptor = mock.Mock()
mock_custom_channel = mock.Mock(spec=grpc.Channel)

with mock.patch(
"google.api_core.grpc_helpers.apply_channel_interceptors",
return_value=mock_custom_channel,
) as mock_apply_interceptors:
transports.SecretManagerServiceGrpcTransport(
channel=mock_custom_channel,
interceptors=[mock_interceptor],
)

mock_apply_interceptors.assert_called_once_with(
mock_custom_channel, [mock_interceptor]
)


@pytest.mark.parametrize(
"client_class,transport_class,transport_name,use_client_cert_env",
[
Expand Down
Loading