Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements identity and scope verification for the Bigtable accelerator daemon before routing RPCs, ensuring that the daemon and client share the same credentials and scopes. If a mismatch is detected, the client refuses to route RPCs, and if verification is inconclusive, it falls back to the native client. A review comment points out a critical issue where failures during server.read_identity() (such as reading from older daemons or JSON parsing errors) are not caught, which would crash client initialization instead of falling back gracefully. A suggestion was provided to catch these exceptions and raise _AcceleratorUnverified.
…tion - Wrap OSError/ValueError from read_identity() as RuntimeError so callers always see a consistent type (not a raw json.JSONDecodeError on truncated identity.json) - Add _AcceleratorIdentityError(RuntimeError) for confirmed identity/scope flips so _maybe_start_accelerator can emit a specific security-oriented warning instead of the generic "Failed to start daemon" message - Fix _resolve_principal() docstring: it does make a link-local metadata-server call for Compute Engine credentials Change-Id: I163a3fbb5342e339896b107c67755ee2838a2003
Wrap server.read_identity() in _verify_daemon_identity with try/except so that any failure (older daemon without identity.json, corrupt/truncated file, permission error) raises _AcceleratorUnverified and triggers the native-client fallback instead of propagating as a raw exception through _start_accelerator. Also fixes AcceleratorDaemon.__init__ to accept an optional binary_path override (primarily for testing), removing pre-existing test failures where _make_daemon was passing the kwarg to a constructor that didn't accept it. Removes the stale git conflict marker from _sync_autogen/client.py. Change-Id: Ib740df045487d07b3a0bda5c1fa3073301776246
| self, | ||
| cli_flags: Sequence[str] = (), | ||
| *, | ||
| binary_path: str | None = None, |
There was a problem hiding this comment.
remove the binary path override.
…cation - Remove binary_path constructor override from AcceleratorDaemon; fix _make_daemon test helper to patch _resolve_binary_path instead - Merge _AcceleratorUnverified and _AcceleratorIdentityError into a single _AcceleratorIdentityError(RuntimeError) with a descriptive message at each raise site; simplify _start_accelerator to a single except BaseException handler and let _maybe_start_accelerator route all identity errors Change-Id: Ieadd5ecd597fbccaf272896fb83533d632e89fd7
sushanb
left a comment
There was a problem hiding this comment.
A few follow-ups from the earlier review — timeout, logging, cache init, and a couple of nits.
| # Compute Engine credentials only populate the email after a refresh | ||
| # against the (link-local, non-egress) metadata server. | ||
| try: | ||
| creds.refresh(google_auth_requests.Request()) |
There was a problem hiding this comment.
google_auth_requests.Request() is constructed with no timeout, and compute_engine.Credentials.refresh doesn't impose one of its own on the metadata call. On a host where 169.254.169.254 is blackholed (hardened VM, broken route, DNS-shadowed metadata.google.internal) this blocks on connect for the OS default — minutes — and because _start_accelerator runs synchronously under _maybe_start_accelerator, client construction hangs for that long.
Please thread a short, bounded timeout here, ideally on the same order as the daemon's own _DEFAULT_STARTUP_TIMEOUT = 10.0:
from google.auth.transport import requests as google_auth_requests
_PRINCIPAL_RESOLVE_TIMEOUT = 2.0 # seconds; metadata server is link-local
...
try:
creds.refresh(google_auth_requests.Request(timeout=_PRINCIPAL_RESOLVE_TIMEOUT))
except Exception as exc:
_LOGGER.debug("Compute Engine credential refresh failed: %s", exc)That way a non-GCE environment that happens to carry CE credentials degrades to principal = None → fall back to native, instead of a multi-minute BigtableDataClient(...) hang.
| try: | ||
| creds.refresh(google_auth_requests.Request()) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
Silent except Exception: pass here hides the reason for refresh failures. If the metadata call fails for a reason other than "no server" (403, token endpoint 5xx, DNS flake), the user ends up with principal = None and sees
Could not verify daemon identity: principal is unknown on this client side
with no breadcrumb back to the real cause. A single debug line costs nothing and makes support bugs tractable:
except Exception as exc:
_LOGGER.debug("Compute Engine credential refresh failed: %s", exc)(Can be folded into the same change as the timeout above.)
| if explicit: | ||
| raise | ||
| warnings.warn( | ||
| f"Accelerator disabled: {exc}", |
There was a problem hiding this comment.
Worth leaving a short comment here spelling out the fail-closed-on-ADC behavior change, since this is the point where it becomes visible to users:
# Any dev running under `gcloud auth application-default login` has no
# service_account_email, so _resolve_principal() returns None and we land
# here with "principal is unknown on this client side". Fail-closed to the
# native client is intentional — user ADC is almost never what the daemon
# resolved — but it does mean every user-ADC caller sees this warning once
# per client construction. Call it out in the release notes.
warnings.warn(
f"Accelerator disabled: {exc}",
RuntimeWarning,
stacklevel=2,
)|
|
||
| Cached: resolution runs at most once per client. | ||
| """ | ||
| cached = getattr(self, "_cached_principal", _UNSET) |
There was a problem hiding this comment.
Nit: _cached_principal is initialized lazily via getattr(..., _UNSET) on first call, which means vars(self) won't show the attribute until then and two concurrent coroutines that both reach a cold _resolve_principal will each run the (potentially network-touching) CE refresh before the second overwrites the first. Low impact — the refresh is idempotent — but a one-liner in _init_accelerator_config makes the invariant explicit and removes the race:
# In _init_accelerator_config, right after self._accelerator_scopes = ...:
self._cached_principal: str | None | object = _UNSETThen _resolve_principal can drop the getattr and just read self._cached_principal.
| pass | ||
| email = getattr(creds, "service_account_email", None) | ||
|
|
||
| principal: str | None = email or None |
There was a problem hiding this comment.
Nit: the three-line principal = email or None / if principal == "default": principal = None reads a bit indirectly — the second check only exists because the first collapsed the CE sentinel into the result. One expression says it:
principal: str | None = email if (email and email != "default") else NoneSame behavior, one branch instead of two.
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
Fixes #<issue_number_goes_here> 🦕