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 @@ -45,7 +45,8 @@
DeadlineExceeded,
ServiceUnavailable,
)
from google.auth import compute_engine
import google.auth
from google.auth import compute_engine, environment_vars
Comment thread
mutianf marked this conversation as resolved.
Resolved
from google.auth.transport import requests as google_auth_requests
from google.cloud.client import ClientWithProject
from google.cloud.environment_vars import BIGTABLE_EMULATOR # type: ignore
Expand Down Expand Up @@ -304,6 +305,20 @@ def __init__(
if project is None:
project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT
self._metrics_interceptor = MetricsInterceptorType()
if (
project is None
and credentials is None
and client_options is not None
and client_options.credentials_file
):
project = os.getenv(
environment_vars.PROJECT,
os.getenv(environment_vars.LEGACY_PROJECT),
)
if project is None:
_, project = google.auth.load_credentials_from_file(
client_options.credentials_file
)
# initialize client
ClientWithProject.__init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
DeadlineExceeded,
ServiceUnavailable,
)
from google.auth import compute_engine
import google.auth
from google.auth import compute_engine, environment_vars
Comment thread
mutianf marked this conversation as resolved.
Resolved
from google.auth.transport import requests as google_auth_requests
from google.cloud.client import ClientWithProject
from google.cloud.environment_vars import BIGTABLE_EMULATOR
Expand Down Expand Up @@ -222,6 +223,20 @@ def __init__(
if project is None:
project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT
self._metrics_interceptor = MetricsInterceptorType()
if (
project is None
and credentials is None
and client_options is not None
and client_options.credentials_file
):
Comment thread
mutianf marked this conversation as resolved.
Resolved
project = os.getenv(
environment_vars.PROJECT,
os.getenv(environment_vars.LEGACY_PROJECT),
)
if project is None:
_, project = google.auth.load_credentials_from_file(
client_options.credentials_file
)
ClientWithProject.__init__(
self,
credentials=credentials,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
client or event loop."""

from types import SimpleNamespace
from unittest import mock

import google.auth
import google.auth.credentials
import pytest
from google.api_core.client_options import ClientOptions
from google.auth import compute_engine
from google.auth import exceptions as auth_exceptions

import google.cloud.bigtable
from google.cloud.bigtable.data._async.client import (
Expand All @@ -29,6 +33,7 @@
_AcceleratorIdentityError,
_DataApiTargetAsync,
)
from google.cloud.bigtable.data._sync_autogen.client import BigtableDataClient


def _bare_client():
Expand Down Expand Up @@ -281,3 +286,181 @@ def test_client_missing_scopes_falls_back(self):
"svc@proj.iam.gserviceaccount.com",
client_scopes=[],
)


class _ScopedFakeCredentials(
google.auth.credentials.Scoped, google.auth.credentials.Credentials
):
def __init__(self, email="sa@file-project.iam.gserviceaccount.com"):
super().__init__()
self.service_account_email = email
self._universe_domain = "googleapis.com"

@property
def universe_domain(self):
return self._universe_domain

@property
def requires_scopes(self):
return False

def with_scopes(self, scopes, default_scopes=None):
return self

def refresh(self, request):
self.token = "fake-token"


class TestCredentialsFileProjectResolution:
"""Verify that ClientOptions(credentials_file=...) resolves client.project
from the credentials file rather than ambient ADC."""

def test_sync_resolves_project_from_credentials_file_instead_of_adc(
self, monkeypatch
):
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.delenv("GCLOUD_PROJECT", raising=False)
monkeypatch.delenv("BIGTABLE_EMULATOR_HOST", raising=False)

file_creds = _ScopedFakeCredentials()
adc_creds = _ScopedFakeCredentials(
"adc@ambient-project.iam.gserviceaccount.com"
)

with (
mock.patch.object(
google.auth,
"load_credentials_from_file",
return_value=(file_creds, "project-from-credentials-file"),
),
mock.patch.object(
google.auth,
"default",
return_value=(adc_creds, "ambient-adc-project"),
) as mock_adc,
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.BigtableMetricsExporter"
),
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.GoogleCloudMetricsHandler"
),
mock.patch.object(BigtableDataClient, "_start_background_channel_refresh"),
):
client = BigtableDataClient(
client_options=ClientOptions(credentials_file="/fake/sa_key.json"),
use_accelerator=False,
)
try:
assert client.project == "project-from-credentials-file"
assert client._accelerator_blocked_reason is None
assert (
client._accelerator_env.get("GOOGLE_APPLICATION_CREDENTIALS")
== "/fake/sa_key.json"
)
mock_adc.assert_not_called()
finally:
client.close()

def test_sync_credentials_file_succeeds_without_ambient_adc(self, monkeypatch):
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.delenv("GCLOUD_PROJECT", raising=False)
monkeypatch.delenv("BIGTABLE_EMULATOR_HOST", raising=False)

file_creds = _ScopedFakeCredentials()

with (
mock.patch.object(
google.auth,
"load_credentials_from_file",
return_value=(file_creds, "project-from-credentials-file"),
),
mock.patch.object(
google.auth,
"default",
side_effect=auth_exceptions.DefaultCredentialsError("No ADC"),
),
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.BigtableMetricsExporter"
),
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.GoogleCloudMetricsHandler"
),
mock.patch.object(BigtableDataClient, "_start_background_channel_refresh"),
):
client = BigtableDataClient(
client_options=ClientOptions(credentials_file="/fake/sa_key.json"),
use_accelerator=False,
)
try:
assert client.project == "project-from-credentials-file"
finally:
client.close()

def test_sync_credentials_file_respects_env_project_override(self, monkeypatch):
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "env-override-project")
monkeypatch.delenv("BIGTABLE_EMULATOR_HOST", raising=False)

file_creds = _ScopedFakeCredentials()

with (
mock.patch.object(
google.auth,
"load_credentials_from_file",
return_value=(file_creds, "project-from-credentials-file"),
),
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.BigtableMetricsExporter"
),
mock.patch(
"google.cloud.bigtable.data._sync_autogen.client.GoogleCloudMetricsHandler"
),
mock.patch.object(BigtableDataClient, "_start_background_channel_refresh"),
):
client = BigtableDataClient(
client_options=ClientOptions(credentials_file="/fake/sa_key.json"),
use_accelerator=False,
)
try:
assert client.project == "env-override-project"
finally:
client.close()

@pytest.mark.asyncio
async def test_async_resolves_project_from_credentials_file_without_adc(
self, monkeypatch
):
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.delenv("GCLOUD_PROJECT", raising=False)
monkeypatch.delenv("BIGTABLE_EMULATOR_HOST", raising=False)

file_creds = _ScopedFakeCredentials()

with (
mock.patch.object(
google.auth,
"load_credentials_from_file",
return_value=(file_creds, "project-from-credentials-file"),
),
mock.patch.object(
google.auth,
"default",
side_effect=auth_exceptions.DefaultCredentialsError("No ADC"),
),
mock.patch(
"google.cloud.bigtable.data._async.client.BigtableMetricsExporter"
),
mock.patch(
"google.cloud.bigtable.data._async.client.GoogleCloudMetricsHandler"
),
mock.patch.object(
BigtableDataClientAsync, "_start_background_channel_refresh"
),
):
client = BigtableDataClientAsync(
client_options=ClientOptions(credentials_file="/fake/sa_key.json"),
use_accelerator=False,
)
try:
assert client.project == "project-from-credentials-file"
finally:
await client.close()
Loading