Skip to content

Commit bb594ae

Browse files
fix(extraction): resolve gcloud ADC HOME for non-nested mounts (#786)
* fix(extraction): resolve gcloud ADC HOME for non-nested mounts _home_for_adc faked HOME by walking two parents up from gcloud_config_mount, assuming the mount is always shaped <home>/.config/gcloud. That holds for dev compose (mounts the developer's real $HOME/.config/gcloud) but not for stage/prod, where the Vault-sourced kartograph-extraction-runtime secret is mounted flatly at /var/secrets/gcloud. There, the derived HOME (/var) has no .config/gcloud, so openshell's --from-gcloud-adc credential resolution fails with "no credentials resolved for provider type 'google-vertex-ai'" even though the ADC file exists at the configured mount. Stage a scratch HOME per invocation with a symlinked .config/gcloud/application_default_credentials.json instead, so credential resolution works regardless of the mount's actual shape. Co-authored-by: Cursor <cursoragent@cursor.com> * style: satisfy ruff format for vertex provider tests Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 43f0bbb commit bb594ae

2 files changed

Lines changed: 88 additions & 12 deletions

File tree

src/api/extraction/infrastructure/openshell/vertex_provider.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import os
7+
import tempfile
78
from contextlib import contextmanager
89
from pathlib import Path
910
from typing import Iterator, Literal
@@ -24,20 +25,32 @@ def _adc_path(*, gcloud_config_mount: str | None) -> Path:
2425

2526
@contextmanager
2627
def _home_for_adc(*, gcloud_config_mount: str | None) -> Iterator[None]:
27-
"""Point HOME at the mounted gcloud directory so ``--from-gcloud-adc`` resolves ADC."""
28+
"""Point HOME at a scratch dir whose ``.config/gcloud`` resolves the mounted ADC.
29+
30+
``--from-gcloud-adc`` resolves credentials via the gcloud SDK convention of
31+
``$HOME/.config/gcloud/application_default_credentials.json``. The mount
32+
itself is not guaranteed to already sit two directories below a usable
33+
HOME (dev compose mounts ``$HOME/.config/gcloud`` directly, but prod
34+
mounts a flat Vault-sourced secret at e.g. ``/var/secrets/gcloud``), so
35+
build the nested layout explicitly instead of assuming one.
36+
"""
2837
if not gcloud_config_mount:
2938
yield
3039
return
31-
home = str(Path(gcloud_config_mount).expanduser().resolve().parent.parent)
32-
previous = os.environ.get("HOME")
33-
os.environ["HOME"] = home
34-
try:
35-
yield
36-
finally:
37-
if previous is None:
38-
os.environ.pop("HOME", None)
39-
else:
40-
os.environ["HOME"] = previous
40+
adc_source = _adc_path(gcloud_config_mount=gcloud_config_mount)
41+
with tempfile.TemporaryDirectory(prefix="kartograph-adc-home-") as scratch_home:
42+
gcloud_dir = Path(scratch_home) / ".config" / "gcloud"
43+
gcloud_dir.mkdir(parents=True, exist_ok=True)
44+
(gcloud_dir / "application_default_credentials.json").symlink_to(adc_source)
45+
previous = os.environ.get("HOME")
46+
os.environ["HOME"] = scratch_home
47+
try:
48+
yield
49+
finally:
50+
if previous is None:
51+
os.environ.pop("HOME", None)
52+
else:
53+
os.environ["HOME"] = previous
4154

4255

4356
def provider_exists(*, provider_name: str) -> bool:

src/api/tests/unit/extraction/infrastructure/test_openshell_vertex_provider.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
from __future__ import annotations
44

5+
import os
6+
from pathlib import Path
57
from unittest.mock import patch
68

79
import pytest
810

911
from extraction.infrastructure.openshell.cli import OpenShellCliError
10-
from extraction.infrastructure.openshell.vertex_provider import ensure_vertex_provider
12+
from extraction.infrastructure.openshell.vertex_provider import (
13+
_home_for_adc,
14+
ensure_vertex_provider,
15+
)
1116

1217

1318
def test_ensure_vertex_provider_skips_when_provider_exists() -> None:
@@ -77,6 +82,64 @@ def test_ensure_vertex_provider_creates_google_vertex_ai_from_adc(tmp_path) -> N
7782
)
7883

7984

85+
def _adc_lookup_path_from_home() -> Path:
86+
"""Where gcloud/OpenShell's ``--from-gcloud-adc`` looks, given the active HOME."""
87+
return (
88+
Path(os.environ["HOME"])
89+
/ ".config"
90+
/ "gcloud"
91+
/ "application_default_credentials.json"
92+
)
93+
94+
95+
def test_home_for_adc_resolves_nested_dev_style_mount(tmp_path) -> None:
96+
"""Dev compose mounts the host's real ``$HOME/.config/gcloud`` directly."""
97+
fake_home = tmp_path / "home" / "dev"
98+
gcloud_dir = fake_home / ".config" / "gcloud"
99+
gcloud_dir.mkdir(parents=True)
100+
adc_file = gcloud_dir / "application_default_credentials.json"
101+
adc_file.write_text('{"type":"authorized_user"}', encoding="utf-8")
102+
103+
with _home_for_adc(gcloud_config_mount=str(gcloud_dir)):
104+
assert _adc_lookup_path_from_home().read_text(encoding="utf-8") == (
105+
adc_file.read_text(encoding="utf-8")
106+
)
107+
108+
109+
def test_home_for_adc_resolves_flat_prod_style_mount(tmp_path) -> None:
110+
"""Prod mounts a Vault-sourced secret flatly (not nested under a home dir).
111+
112+
Regression test: OpenShift mounts the ``kartograph-extraction-runtime``
113+
ExternalSecret at ``/var/secrets/gcloud`` (see hp-fleet-gitops
114+
apps/kartograph/base/api-deployment.yaml), so the mount is *not* two
115+
directories below a usable HOME. ``_home_for_adc`` must still produce a
116+
HOME whose ``.config/gcloud/application_default_credentials.json``
117+
resolves to the actual mounted ADC file.
118+
"""
119+
flat_mount = tmp_path / "var" / "secrets" / "gcloud"
120+
flat_mount.mkdir(parents=True)
121+
adc_file = flat_mount / "application_default_credentials.json"
122+
adc_file.write_text('{"type":"service_account"}', encoding="utf-8")
123+
124+
with _home_for_adc(gcloud_config_mount=str(flat_mount)):
125+
assert _adc_lookup_path_from_home().read_text(encoding="utf-8") == (
126+
adc_file.read_text(encoding="utf-8")
127+
)
128+
129+
130+
def test_home_for_adc_restores_previous_home(tmp_path) -> None:
131+
flat_mount = tmp_path / "secrets" / "gcloud"
132+
flat_mount.mkdir(parents=True)
133+
(flat_mount / "application_default_credentials.json").write_text(
134+
"{}", encoding="utf-8"
135+
)
136+
137+
previous = os.environ.get("HOME")
138+
with _home_for_adc(gcloud_config_mount=str(flat_mount)):
139+
assert os.environ.get("HOME") != previous
140+
assert os.environ.get("HOME") == previous
141+
142+
80143
def test_ensure_vertex_provider_raises_when_adc_missing(tmp_path) -> None:
81144
with patch(
82145
"extraction.infrastructure.openshell.vertex_provider.provider_exists",

0 commit comments

Comments
 (0)