Skip to content

Commit 53c2cbf

Browse files
shollymanLinchin
andauthored
fix: augment universe_domain handling (#1837)
* fix: augment universe_domain handling This PR revisits the universe resolution for the BQ client, and handles new requirements like env-based specification and validation. * lint * skipif core too old * deps * add import * no-cover in test helper * lint * ignore google.auth typing * capitalization * change to raise in test code * reviewer feedback * var fix --------- Co-authored-by: Lingqing Gan <lingqing.gan@gmail.com>
1 parent b359a9a commit 53c2cbf

File tree

4 files changed

+162
-9
lines changed

4 files changed

+162
-9
lines changed

‎google/cloud/bigquery/_helpers.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from google.cloud._helpers import _RFC3339_MICROS
3131
from google.cloud._helpers import _RFC3339_NO_FRACTION
3232
from google.cloud._helpers import _to_bytes
33+
from google.auth import credentials as ga_credentials # type: ignore
34+
from google.api_core import client_options as client_options_lib
3335

3436
_RFC3339_MICROS_NO_ZULU = "%Y-%m-%dT%H:%M:%S.%f"
3537
_TIMEONLY_WO_MICROS = "%H:%M:%S"
@@ -55,9 +57,63 @@
5557
_DEFAULT_HOST = "https://bigquery.googleapis.com"
5658
"""Default host for JSON API."""
5759

60+
_DEFAULT_HOST_TEMPLATE = "https://bigquery.{UNIVERSE_DOMAIN}"
61+
""" Templatized endpoint format. """
62+
5863
_DEFAULT_UNIVERSE = "googleapis.com"
5964
"""Default universe for the JSON API."""
6065

66+
_UNIVERSE_DOMAIN_ENV = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
67+
"""Environment variable for setting universe domain."""
68+
69+
70+
def _get_client_universe(
71+
client_options: Optional[Union[client_options_lib.ClientOptions, dict]]
72+
) -> str:
73+
"""Retrieves the specified universe setting.
74+
75+
Args:
76+
client_options: specified client options.
77+
Returns:
78+
str: resolved universe setting.
79+
80+
"""
81+
if isinstance(client_options, dict):
82+
client_options = client_options_lib.from_dict(client_options)
83+
universe = _DEFAULT_UNIVERSE
84+
if hasattr(client_options, "universe_domain"):
85+
options_universe = getattr(client_options, "universe_domain")
86+
if options_universe is not None and len(options_universe) > 0:
87+
universe = options_universe
88+
else:
89+
env_universe = os.getenv(_UNIVERSE_DOMAIN_ENV)
90+
if isinstance(env_universe, str) and len(env_universe) > 0:
91+
universe = env_universe
92+
return universe
93+
94+
95+
def _validate_universe(client_universe: str, credentials: ga_credentials.Credentials):
96+
"""Validates that client provided universe and universe embedded in credentials match.
97+
98+
Args:
99+
client_universe (str): The universe domain configured via the client options.
100+
credentials (ga_credentials.Credentials): The credentials being used in the client.
101+
102+
Raises:
103+
ValueError: when client_universe does not match the universe in credentials.
104+
"""
105+
if hasattr(credentials, "universe_domain"):
106+
cred_universe = getattr(credentials, "universe_domain")
107+
if isinstance(cred_universe, str):
108+
if client_universe != cred_universe:
109+
raise ValueError(
110+
"The configured universe domain "
111+
f"({client_universe}) does not match the universe domain "
112+
f"found in the credentials ({cred_universe}). "
113+
"If you haven't configured the universe domain explicitly, "
114+
f"`{_DEFAULT_UNIVERSE}` is the default."
115+
)
116+
61117

62118
def _get_bigquery_host():
63119
return os.environ.get(BIGQUERY_EMULATOR_HOST, _DEFAULT_HOST)

‎google/cloud/bigquery/client.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@
7878
from google.cloud.bigquery._helpers import _verify_job_config_type
7979
from google.cloud.bigquery._helpers import _get_bigquery_host
8080
from google.cloud.bigquery._helpers import _DEFAULT_HOST
81+
from google.cloud.bigquery._helpers import _DEFAULT_HOST_TEMPLATE
8182
from google.cloud.bigquery._helpers import _DEFAULT_UNIVERSE
83+
from google.cloud.bigquery._helpers import _validate_universe
84+
from google.cloud.bigquery._helpers import _get_client_universe
8285
from google.cloud.bigquery._job_helpers import make_job_id as _make_job_id
8386
from google.cloud.bigquery.dataset import Dataset
8487
from google.cloud.bigquery.dataset import DatasetListItem
@@ -245,6 +248,7 @@ def __init__(
245248
kw_args = {"client_info": client_info}
246249
bq_host = _get_bigquery_host()
247250
kw_args["api_endpoint"] = bq_host if bq_host != _DEFAULT_HOST else None
251+
client_universe = None
248252
if client_options:
249253
if isinstance(client_options, dict):
250254
client_options = google.api_core.client_options.from_dict(
@@ -253,14 +257,15 @@ def __init__(
253257
if client_options.api_endpoint:
254258
api_endpoint = client_options.api_endpoint
255259
kw_args["api_endpoint"] = api_endpoint
256-
elif (
257-
hasattr(client_options, "universe_domain")
258-
and client_options.universe_domain
259-
and client_options.universe_domain is not _DEFAULT_UNIVERSE
260-
):
261-
kw_args["api_endpoint"] = _DEFAULT_HOST.replace(
262-
_DEFAULT_UNIVERSE, client_options.universe_domain
263-
)
260+
else:
261+
client_universe = _get_client_universe(client_options)
262+
if client_universe != _DEFAULT_UNIVERSE:
263+
kw_args["api_endpoint"] = _DEFAULT_HOST_TEMPLATE.replace(
264+
"{UNIVERSE_DOMAIN}", client_universe
265+
)
266+
# Ensure credentials and universe are not in conflict.
267+
if hasattr(self, "_credentials") and client_universe is not None:
268+
_validate_universe(client_universe, self._credentials)
264269

265270
self._connection = Connection(self, **kw_args)
266271
self._location = location

‎tests/unit/helpers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ def make_client(project="PROJECT", **kw):
4343
return google.cloud.bigquery.client.Client(project, credentials, **kw)
4444

4545

46+
def make_creds(creds_universe: None):
47+
from google.auth import credentials
48+
49+
class TestingCreds(credentials.Credentials):
50+
def refresh(self, request): # pragma: NO COVER
51+
raise NotImplementedError
52+
53+
@property
54+
def universe_domain(self):
55+
return creds_universe
56+
57+
return TestingCreds()
58+
59+
4660
def make_dataset_reference_string(project, ds_id):
4761
return f"{project}.{ds_id}"
4862

‎tests/unit/test__helpers.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,86 @@
1717
import decimal
1818
import json
1919
import unittest
20-
20+
import os
2121
import mock
22+
import pytest
23+
import packaging
24+
import google.api_core
25+
26+
27+
@pytest.mark.skipif(
28+
packaging.version.parse(getattr(google.api_core, "__version__", "0.0.0"))
29+
< packaging.version.Version("2.15.0"),
30+
reason="universe_domain not supported with google-api-core < 2.15.0",
31+
)
32+
class Test_get_client_universe(unittest.TestCase):
33+
def test_with_none(self):
34+
from google.cloud.bigquery._helpers import _get_client_universe
35+
36+
self.assertEqual("googleapis.com", _get_client_universe(None))
37+
38+
def test_with_dict(self):
39+
from google.cloud.bigquery._helpers import _get_client_universe
40+
41+
options = {"universe_domain": "foo.com"}
42+
self.assertEqual("foo.com", _get_client_universe(options))
43+
44+
def test_with_dict_empty(self):
45+
from google.cloud.bigquery._helpers import _get_client_universe
46+
47+
options = {"universe_domain": ""}
48+
self.assertEqual("googleapis.com", _get_client_universe(options))
49+
50+
def test_with_client_options(self):
51+
from google.cloud.bigquery._helpers import _get_client_universe
52+
from google.api_core import client_options
53+
54+
options = client_options.from_dict({"universe_domain": "foo.com"})
55+
self.assertEqual("foo.com", _get_client_universe(options))
56+
57+
@mock.patch.dict(os.environ, {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "foo.com"})
58+
def test_with_environ(self):
59+
from google.cloud.bigquery._helpers import _get_client_universe
60+
61+
self.assertEqual("foo.com", _get_client_universe(None))
62+
63+
@mock.patch.dict(os.environ, {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": ""})
64+
def test_with_environ_empty(self):
65+
from google.cloud.bigquery._helpers import _get_client_universe
66+
67+
self.assertEqual("googleapis.com", _get_client_universe(None))
68+
69+
70+
class Test_validate_universe(unittest.TestCase):
71+
def test_with_none(self):
72+
from google.cloud.bigquery._helpers import _validate_universe
73+
74+
# should not raise
75+
_validate_universe("googleapis.com", None)
76+
77+
def test_with_no_universe_creds(self):
78+
from google.cloud.bigquery._helpers import _validate_universe
79+
from .helpers import make_creds
80+
81+
creds = make_creds(None)
82+
# should not raise
83+
_validate_universe("googleapis.com", creds)
84+
85+
def test_with_matched_universe_creds(self):
86+
from google.cloud.bigquery._helpers import _validate_universe
87+
from .helpers import make_creds
88+
89+
creds = make_creds("googleapis.com")
90+
# should not raise
91+
_validate_universe("googleapis.com", creds)
92+
93+
def test_with_mismatched_universe_creds(self):
94+
from google.cloud.bigquery._helpers import _validate_universe
95+
from .helpers import make_creds
96+
97+
creds = make_creds("foo.com")
98+
with self.assertRaises(ValueError):
99+
_validate_universe("googleapis.com", creds)
22100

23101

24102
class Test_not_null(unittest.TestCase):

0 commit comments

Comments
 (0)