Fix Vertex ADC discovery on Windows
- commit
- bcb7dc3c9ea3021fabb7d419bf64b4373a637b7b
- author
- patriceckhart · · 2026-07-14 11:58 UTC
- parent
- 2806fa1
- signature
- unsigned
6 files · +93 -29
M README.md +12 -7
| @@ -554,9 +554,9 @@ Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also conf | |||
| 554 | 554 | ||
| 555 | 555 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | |
| 556 | 556 | ||
| 557 | - | ### Google Enterprise Agent Platform (formerly Google Vertex AI) | |
| 557 | + | ### Gemini Enterprise Agent Platform (formerly Google Vertex AI) | |
| 558 | 558 | ||
| 559 | - | zot also has built-in support for [Google Enterprise Agent Platform](https://cloud.google.com/products/gemini-enterprise-agent-platform), Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio `google` provider above, Google Enterprise Agent Platform supports Google Cloud service-account and Application Default Credentials (ADC) auth in addition to a plain API key — no consumer Google login, but full GCP-style credential support. | |
| 559 | + | zot also has built-in support for [Gemini Enterprise Agent Platform](https://cloud.google.com/products/gemini-enterprise-agent-platform), Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio `google` provider above, the `google-vertex` provider supports a Google Cloud API key plus `service_account` and `authorized_user` credential files. It does not support consumer Google login or the complete ADC credential chain, such as metadata-server and workload-identity credentials. | |
| 560 | 560 | ||
| 561 | 561 | ```bash | |
| 562 | 562 | zot --provider google-vertex | |
| @@ -569,11 +569,16 @@ Required configuration (env vars, read at construction time): | |||
| 569 | 569 | ||
| 570 | 570 | Credential lookup order for Vertex: | |
| 571 | 571 | ||
| 572 | - | 1. `GOOGLE_CLOUD_API_KEY` — simplest option; an API key created in the GCP console, sent as `x-goog-api-key`. Used directly, no token exchange. | |
| 573 | - | 2. `GOOGLE_APPLICATION_CREDENTIALS` — path to a service-account JSON key. zot signs a JWT with the key's private key and exchanges it at `https://oauth2.googleapis.com/token` for a short-lived (1h) access token, cached in memory and refreshed on demand. | |
| 574 | - | 3. **Default ADC file** — if neither of the above is set, zot falls back to `~/.config/gcloud/application_default_credentials.json`, i.e. whatever `gcloud auth application-default login` produced. Both credential shapes found there are supported: | |
| 575 | - | - `type: "service_account"` — same JWT/token-exchange flow as above. | |
| 576 | - | - `type: "authorized_user"` — the user-OAuth shape gcloud writes; zot reads the `client_id`/`client_secret`/`refresh_token` and exchanges the refresh token for an access token, with the same caching/refresh behavior. | |
| 572 | + | 1. `GOOGLE_CLOUD_API_KEY`: simplest option. An API key created in the GCP console is sent as `x-goog-api-key`, without a token exchange. | |
| 573 | + | 2. `GOOGLE_APPLICATION_CREDENTIALS`: path to a supported credential JSON file. | |
| 574 | + | 3. **Default ADC file**: if neither of the above is set, zot checks the file written by `gcloud auth application-default login`. This is `~/.config/gcloud/application_default_credentials.json` on Unix systems and `%APPDATA%\gcloud\application_default_credentials.json` on Windows. | |
| 575 | + | ||
| 576 | + | Two credential file shapes are supported: | |
| 577 | + | ||
| 578 | + | - `type: "service_account"`: zot signs a JWT with the private key and exchanges it for a short-lived access token. | |
| 579 | + | - `type: "authorized_user"`: zot exchanges the stored client ID, client secret, and refresh token for a short-lived access token. | |
| 580 | + | ||
| 581 | + | Access tokens are cached in memory and refreshed on demand. | |
| 577 | 582 | ||
| 578 | 583 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | |
| 579 | 584 | ||
| @@ -554,9 +554,9 @@ Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also conf | |||||
| 554 | 554 | ||||
| 555 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | 555 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | ||
| 556 | 556 | ||||
| 557 | - | ### Google Enterprise Agent Platform (formerly Google Vertex AI) | 557 | + | ### Gemini Enterprise Agent Platform (formerly Google Vertex AI) |
| 558 | 558 | ||||
| 559 | - | zot also has built-in support for [Google Enterprise Agent Platform](https://cloud.google.com/products/gemini-enterprise-agent-platform), Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio `google` provider above, Google Enterprise Agent Platform supports Google Cloud service-account and Application Default Credentials (ADC) auth in addition to a plain API key — no consumer Google login, but full GCP-style credential support. | 559 | + | zot also has built-in support for [Gemini Enterprise Agent Platform](https://cloud.google.com/products/gemini-enterprise-agent-platform), Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio `google` provider above, the `google-vertex` provider supports a Google Cloud API key plus `service_account` and `authorized_user` credential files. It does not support consumer Google login or the complete ADC credential chain, such as metadata-server and workload-identity credentials. |
| 560 | 560 | ||||
| 561 | ```bash | 561 | ```bash | ||
| 562 | zot --provider google-vertex | 562 | zot --provider google-vertex | ||
| @@ -569,11 +569,16 @@ Required configuration (env vars, read at construction time): | |||||
| 569 | 569 | ||||
| 570 | Credential lookup order for Vertex: | 570 | Credential lookup order for Vertex: | ||
| 571 | 571 | ||||
| 572 | - | 1. `GOOGLE_CLOUD_API_KEY` — simplest option; an API key created in the GCP console, sent as `x-goog-api-key`. Used directly, no token exchange. | 572 | + | 1. `GOOGLE_CLOUD_API_KEY`: simplest option. An API key created in the GCP console is sent as `x-goog-api-key`, without a token exchange. |
| 573 | - | 2. `GOOGLE_APPLICATION_CREDENTIALS` — path to a service-account JSON key. zot signs a JWT with the key's private key and exchanges it at `https://oauth2.googleapis.com/token` for a short-lived (1h) access token, cached in memory and refreshed on demand. | 573 | + | 2. `GOOGLE_APPLICATION_CREDENTIALS`: path to a supported credential JSON file. |
| 574 | - | 3. **Default ADC file** — if neither of the above is set, zot falls back to `~/.config/gcloud/application_default_credentials.json`, i.e. whatever `gcloud auth application-default login` produced. Both credential shapes found there are supported: | 574 | + | 3. **Default ADC file**: if neither of the above is set, zot checks the file written by `gcloud auth application-default login`. This is `~/.config/gcloud/application_default_credentials.json` on Unix systems and `%APPDATA%\gcloud\application_default_credentials.json` on Windows. |
| 575 | - | - `type: "service_account"` — same JWT/token-exchange flow as above. | 575 | + | |
| 576 | - | - `type: "authorized_user"` — the user-OAuth shape gcloud writes; zot reads the `client_id`/`client_secret`/`refresh_token` and exchanges the refresh token for an access token, with the same caching/refresh behavior. | 576 | + | Two credential file shapes are supported: |
| 577 | + | ||||
| 578 | + | - `type: "service_account"`: zot signs a JWT with the private key and exchanges it for a short-lived access token. | |||
| 579 | + | - `type: "authorized_user"`: zot exchanges the stored client ID, client secret, and refresh token for a short-lived access token. | |||
| 580 | + | ||||
| 581 | + | Access tokens are cached in memory and refreshed on demand. | |||
| 577 | 582 | ||||
| 578 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | 583 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | ||
| 579 | 584 | ||||
M packages/agent/config.go +2 -3
| @@ -381,9 +381,8 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s | |||
| 381 | 381 | if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { | |
| 382 | 382 | return "<adc>", "apikey", "", nil | |
| 383 | 383 | } | |
| 384 | - | // Check for the default ADC path that gcloud writes to | |
| 385 | - | if home, err := os.UserHomeDir(); err == nil { | |
| 386 | - | adcPath := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json") | |
| 384 | + | // Check for the platform-specific default ADC path that gcloud writes to. | |
| 385 | + | if adcPath, err := auth.GoogleApplicationDefaultCredentialsPath(); err == nil { | |
| 387 | 386 | if _, err := os.Stat(adcPath); err == nil { | |
| 388 | 387 | return "<adc>", "apikey", "", nil | |
| 389 | 388 | } | |
| @@ -381,9 +381,8 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s | |||||
| 381 | if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { | 381 | if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { | ||
| 382 | return "<adc>", "apikey", "", nil | 382 | return "<adc>", "apikey", "", nil | ||
| 383 | } | 383 | } | ||
| 384 | - | // Check for the default ADC path that gcloud writes to | 384 | + | // Check for the platform-specific default ADC path that gcloud writes to. |
| 385 | - | if home, err := os.UserHomeDir(); err == nil { | 385 | + | if adcPath, err := auth.GoogleApplicationDefaultCredentialsPath(); err == nil { |
| 386 | - | adcPath := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json") | |||
| 387 | if _, err := os.Stat(adcPath); err == nil { | 386 | if _, err := os.Stat(adcPath); err == nil { | ||
| 388 | return "<adc>", "apikey", "", nil | 387 | return "<adc>", "apikey", "", nil | ||
| 389 | } | 388 | } | ||
M packages/agent/config_gcp_test.go +23 -17
| @@ -4,14 +4,28 @@ import ( | |||
| 4 | 4 | "os" | |
| 5 | 5 | "path/filepath" | |
| 6 | 6 | "testing" | |
| 7 | + | ||
| 8 | + | providerauth "github.com/patriceckhart/zot/packages/provider/auth" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 11 | + | func isolateGoogleVertexADC(t *testing.T) string { | |
| 12 | + | t.Helper() | |
| 13 | + | root := t.TempDir() | |
| 14 | + | t.Setenv("HOME", root) | |
| 15 | + | t.Setenv("APPDATA", root) | |
| 16 | + | path, err := providerauth.GoogleApplicationDefaultCredentialsPath() | |
| 17 | + | if err != nil { | |
| 18 | + | t.Fatalf("GoogleApplicationDefaultCredentialsPath: %v", err) | |
| 19 | + | } | |
| 20 | + | return path | |
| 21 | + | } | |
| 22 | + | ||
| 9 | 23 | // TestResolveCredentialGoogleVertexAPIKey confirms that GOOGLE_CLOUD_API_KEY | |
| 10 | 24 | // takes top priority for the google-vertex provider and is returned with | |
| 11 | 25 | // method "apikey". | |
| 12 | 26 | func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) { | |
| 13 | 27 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 14 | - | t.Setenv("HOME", t.TempDir()) | |
| 28 | + | isolateGoogleVertexADC(t) | |
| 15 | 29 | t.Setenv("GOOGLE_CLOUD_API_KEY", "gcp-key-123") | |
| 16 | 30 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | |
| 17 | 31 | ||
| @@ -33,7 +47,7 @@ func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) { | |||
| 33 | 47 | // the real NewVertex client's ADC-based auth flow. | |
| 34 | 48 | func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) { | |
| 35 | 49 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 36 | - | t.Setenv("HOME", t.TempDir()) | |
| 50 | + | isolateGoogleVertexADC(t) | |
| 37 | 51 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | |
| 38 | 52 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/service-account.json") | |
| 39 | 53 | ||
| @@ -50,21 +64,17 @@ func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) { | |||
| 50 | 64 | } | |
| 51 | 65 | ||
| 52 | 66 | // TestResolveCredentialGoogleVertexDefaultADCFile confirms that, absent any | |
| 53 | - | // env vars, the presence of the default gcloud ADC file | |
| 54 | - | // (~/.config/gcloud/application_default_credentials.json) is detected and | |
| 55 | - | // resolves to the "<adc>" sentinel. | |
| 67 | + | // env vars, the presence of the platform-specific default gcloud ADC file is | |
| 68 | + | // detected and resolves to the "<adc>" sentinel. | |
| 56 | 69 | func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) { | |
| 57 | - | home := t.TempDir() | |
| 58 | 70 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 59 | - | t.Setenv("HOME", home) | |
| 71 | + | adcPath := isolateGoogleVertexADC(t) | |
| 60 | 72 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | |
| 61 | 73 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | |
| 62 | 74 | ||
| 63 | - | adcDir := filepath.Join(home, ".config", "gcloud") | |
| 64 | - | if err := os.MkdirAll(adcDir, 0o755); err != nil { | |
| 75 | + | if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil { | |
| 65 | 76 | t.Fatal(err) | |
| 66 | 77 | } | |
| 67 | - | adcPath := filepath.Join(adcDir, "application_default_credentials.json") | |
| 68 | 78 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | |
| 69 | 79 | t.Fatal(err) | |
| 70 | 80 | } | |
| @@ -85,17 +95,14 @@ func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) { | |||
| 85 | 95 | // wins over GOOGLE_APPLICATION_CREDENTIALS and the default ADC file when | |
| 86 | 96 | // multiple credential sources are present simultaneously. | |
| 87 | 97 | func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) { | |
| 88 | - | home := t.TempDir() | |
| 89 | 98 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 90 | - | t.Setenv("HOME", home) | |
| 99 | + | adcPath := isolateGoogleVertexADC(t) | |
| 91 | 100 | t.Setenv("GOOGLE_CLOUD_API_KEY", "explicit-key") | |
| 92 | 101 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/sa.json") | |
| 93 | 102 | ||
| 94 | - | adcDir := filepath.Join(home, ".config", "gcloud") | |
| 95 | - | if err := os.MkdirAll(adcDir, 0o755); err != nil { | |
| 103 | + | if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil { | |
| 96 | 104 | t.Fatal(err) | |
| 97 | 105 | } | |
| 98 | - | adcPath := filepath.Join(adcDir, "application_default_credentials.json") | |
| 99 | 106 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | |
| 100 | 107 | t.Fatal(err) | |
| 101 | 108 | } | |
| @@ -117,9 +124,8 @@ func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) { | |||
| 117 | 124 | // auth.json entry), resolution fails with an error rather than silently | |
| 118 | 125 | // returning an empty credential. | |
| 119 | 126 | func TestResolveCredentialGoogleVertexNoCredentials(t *testing.T) { | |
| 120 | - | home := t.TempDir() | |
| 121 | 127 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 122 | - | t.Setenv("HOME", home) | |
| 128 | + | isolateGoogleVertexADC(t) | |
| 123 | 129 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | |
| 124 | 130 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | |
| 125 | 131 | ||
| @@ -4,14 +4,28 @@ import ( | |||||
| 4 | "os" | 4 | "os" | ||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | ||||
| 8 | + | providerauth "github.com/patriceckhart/zot/packages/provider/auth" | |||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| 11 | + | func isolateGoogleVertexADC(t *testing.T) string { | |||
| 12 | + | t.Helper() | |||
| 13 | + | root := t.TempDir() | |||
| 14 | + | t.Setenv("HOME", root) | |||
| 15 | + | t.Setenv("APPDATA", root) | |||
| 16 | + | path, err := providerauth.GoogleApplicationDefaultCredentialsPath() | |||
| 17 | + | if err != nil { | |||
| 18 | + | t.Fatalf("GoogleApplicationDefaultCredentialsPath: %v", err) | |||
| 19 | + | } | |||
| 20 | + | return path | |||
| 21 | + | } | |||
| 22 | + | ||||
| 9 | // TestResolveCredentialGoogleVertexAPIKey confirms that GOOGLE_CLOUD_API_KEY | 23 | // TestResolveCredentialGoogleVertexAPIKey confirms that GOOGLE_CLOUD_API_KEY | ||
| 10 | // takes top priority for the google-vertex provider and is returned with | 24 | // takes top priority for the google-vertex provider and is returned with | ||
| 11 | // method "apikey". | 25 | // method "apikey". | ||
| 12 | func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) { | 26 | func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) { | ||
| 13 | t.Setenv("ZOT_HOME", t.TempDir()) | 27 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 14 | - | t.Setenv("HOME", t.TempDir()) | 28 | + | isolateGoogleVertexADC(t) |
| 15 | t.Setenv("GOOGLE_CLOUD_API_KEY", "gcp-key-123") | 29 | t.Setenv("GOOGLE_CLOUD_API_KEY", "gcp-key-123") | ||
| 16 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | 30 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | ||
| 17 | 31 | ||||
| @@ -33,7 +47,7 @@ func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) { | |||||
| 33 | // the real NewVertex client's ADC-based auth flow. | 47 | // the real NewVertex client's ADC-based auth flow. | ||
| 34 | func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) { | 48 | func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) { | ||
| 35 | t.Setenv("ZOT_HOME", t.TempDir()) | 49 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 36 | - | t.Setenv("HOME", t.TempDir()) | 50 | + | isolateGoogleVertexADC(t) |
| 37 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | 51 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | ||
| 38 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/service-account.json") | 52 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/service-account.json") | ||
| 39 | 53 | ||||
| @@ -50,21 +64,17 @@ func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) { | |||||
| 50 | } | 64 | } | ||
| 51 | 65 | ||||
| 52 | // TestResolveCredentialGoogleVertexDefaultADCFile confirms that, absent any | 66 | // TestResolveCredentialGoogleVertexDefaultADCFile confirms that, absent any | ||
| 53 | - | // env vars, the presence of the default gcloud ADC file | 67 | + | // env vars, the presence of the platform-specific default gcloud ADC file is |
| 54 | - | // (~/.config/gcloud/application_default_credentials.json) is detected and | 68 | + | // detected and resolves to the "<adc>" sentinel. |
| 55 | - | // resolves to the "<adc>" sentinel. | |||
| 56 | func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) { | 69 | func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) { | ||
| 57 | - | home := t.TempDir() | |||
| 58 | t.Setenv("ZOT_HOME", t.TempDir()) | 70 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 59 | - | t.Setenv("HOME", home) | 71 | + | adcPath := isolateGoogleVertexADC(t) |
| 60 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | 72 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | ||
| 61 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | 73 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | ||
| 62 | 74 | ||||
| 63 | - | adcDir := filepath.Join(home, ".config", "gcloud") | 75 | + | if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil { |
| 64 | - | if err := os.MkdirAll(adcDir, 0o755); err != nil { | |||
| 65 | t.Fatal(err) | 76 | t.Fatal(err) | ||
| 66 | } | 77 | } | ||
| 67 | - | adcPath := filepath.Join(adcDir, "application_default_credentials.json") | |||
| 68 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | 78 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | ||
| 69 | t.Fatal(err) | 79 | t.Fatal(err) | ||
| 70 | } | 80 | } | ||
| @@ -85,17 +95,14 @@ func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) { | |||||
| 85 | // wins over GOOGLE_APPLICATION_CREDENTIALS and the default ADC file when | 95 | // wins over GOOGLE_APPLICATION_CREDENTIALS and the default ADC file when | ||
| 86 | // multiple credential sources are present simultaneously. | 96 | // multiple credential sources are present simultaneously. | ||
| 87 | func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) { | 97 | func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) { | ||
| 88 | - | home := t.TempDir() | |||
| 89 | t.Setenv("ZOT_HOME", t.TempDir()) | 98 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 90 | - | t.Setenv("HOME", home) | 99 | + | adcPath := isolateGoogleVertexADC(t) |
| 91 | t.Setenv("GOOGLE_CLOUD_API_KEY", "explicit-key") | 100 | t.Setenv("GOOGLE_CLOUD_API_KEY", "explicit-key") | ||
| 92 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/sa.json") | 101 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/sa.json") | ||
| 93 | 102 | ||||
| 94 | - | adcDir := filepath.Join(home, ".config", "gcloud") | 103 | + | if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil { |
| 95 | - | if err := os.MkdirAll(adcDir, 0o755); err != nil { | |||
| 96 | t.Fatal(err) | 104 | t.Fatal(err) | ||
| 97 | } | 105 | } | ||
| 98 | - | adcPath := filepath.Join(adcDir, "application_default_credentials.json") | |||
| 99 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | 106 | if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil { | ||
| 100 | t.Fatal(err) | 107 | t.Fatal(err) | ||
| 101 | } | 108 | } | ||
| @@ -117,9 +124,8 @@ func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) { | |||||
| 117 | // auth.json entry), resolution fails with an error rather than silently | 124 | // auth.json entry), resolution fails with an error rather than silently | ||
| 118 | // returning an empty credential. | 125 | // returning an empty credential. | ||
| 119 | func TestResolveCredentialGoogleVertexNoCredentials(t *testing.T) { | 126 | func TestResolveCredentialGoogleVertexNoCredentials(t *testing.T) { | ||
| 120 | - | home := t.TempDir() | |||
| 121 | t.Setenv("ZOT_HOME", t.TempDir()) | 127 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 122 | - | t.Setenv("HOME", home) | 128 | + | isolateGoogleVertexADC(t) |
| 123 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | 129 | t.Setenv("GOOGLE_CLOUD_API_KEY", "") | ||
| 124 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | 130 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") | ||
| 125 | 131 | ||||
A packages/provider/auth/google_adc.go +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | package auth | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 6 | + | "runtime" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // GoogleApplicationDefaultCredentialsPath returns the default path written by | |
| 10 | + | // gcloud auth application-default login. | |
| 11 | + | func GoogleApplicationDefaultCredentialsPath() (string, error) { | |
| 12 | + | if runtime.GOOS == "windows" { | |
| 13 | + | configDir, err := os.UserConfigDir() | |
| 14 | + | if err != nil { | |
| 15 | + | return "", err | |
| 16 | + | } | |
| 17 | + | return filepath.Join(configDir, "gcloud", "application_default_credentials.json"), nil | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | home, err := os.UserHomeDir() | |
| 21 | + | if err != nil { | |
| 22 | + | return "", err | |
| 23 | + | } | |
| 24 | + | return filepath.Join(home, ".config", "gcloud", "application_default_credentials.json"), nil | |
| 25 | + | } | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | package auth | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "runtime" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // GoogleApplicationDefaultCredentialsPath returns the default path written by | |||
| 10 | + | // gcloud auth application-default login. | |||
| 11 | + | func GoogleApplicationDefaultCredentialsPath() (string, error) { | |||
| 12 | + | if runtime.GOOS == "windows" { | |||
| 13 | + | configDir, err := os.UserConfigDir() | |||
| 14 | + | if err != nil { | |||
| 15 | + | return "", err | |||
| 16 | + | } | |||
| 17 | + | return filepath.Join(configDir, "gcloud", "application_default_credentials.json"), nil | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | home, err := os.UserHomeDir() | |||
| 21 | + | if err != nil { | |||
| 22 | + | return "", err | |||
| 23 | + | } | |||
| 24 | + | return filepath.Join(home, ".config", "gcloud", "application_default_credentials.json"), nil | |||
| 25 | + | } | |||
A packages/provider/auth/google_adc_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package auth | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "path/filepath" | |
| 5 | + | "runtime" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestGoogleApplicationDefaultCredentialsPath(t *testing.T) { | |
| 10 | + | root := t.TempDir() | |
| 11 | + | t.Setenv("HOME", root) | |
| 12 | + | t.Setenv("APPDATA", root) | |
| 13 | + | ||
| 14 | + | got, err := GoogleApplicationDefaultCredentialsPath() | |
| 15 | + | if err != nil { | |
| 16 | + | t.Fatalf("GoogleApplicationDefaultCredentialsPath: %v", err) | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | var want string | |
| 20 | + | if runtime.GOOS == "windows" { | |
| 21 | + | want = filepath.Join(root, "gcloud", "application_default_credentials.json") | |
| 22 | + | } else { | |
| 23 | + | want = filepath.Join(root, ".config", "gcloud", "application_default_credentials.json") | |
| 24 | + | } | |
| 25 | + | if got != want { | |
| 26 | + | t.Fatalf("path = %q, want %q", got, want) | |
| 27 | + | } | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package auth | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "path/filepath" | |||
| 5 | + | "runtime" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestGoogleApplicationDefaultCredentialsPath(t *testing.T) { | |||
| 10 | + | root := t.TempDir() | |||
| 11 | + | t.Setenv("HOME", root) | |||
| 12 | + | t.Setenv("APPDATA", root) | |||
| 13 | + | ||||
| 14 | + | got, err := GoogleApplicationDefaultCredentialsPath() | |||
| 15 | + | if err != nil { | |||
| 16 | + | t.Fatalf("GoogleApplicationDefaultCredentialsPath: %v", err) | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | var want string | |||
| 20 | + | if runtime.GOOS == "windows" { | |||
| 21 | + | want = filepath.Join(root, "gcloud", "application_default_credentials.json") | |||
| 22 | + | } else { | |||
| 23 | + | want = filepath.Join(root, ".config", "gcloud", "application_default_credentials.json") | |||
| 24 | + | } | |||
| 25 | + | if got != want { | |||
| 26 | + | t.Fatalf("path = %q, want %q", got, want) | |||
| 27 | + | } | |||
| 28 | + | } | |||
M packages/provider/google_vertex.go +3 -2
| @@ -51,6 +51,8 @@ import ( | |||
| 51 | 51 | "strings" | |
| 52 | 52 | "sync" | |
| 53 | 53 | "time" | |
| 54 | + | ||
| 55 | + | providerauth "github.com/patriceckhart/zot/packages/provider/auth" | |
| 54 | 56 | ) | |
| 55 | 57 | ||
| 56 | 58 | type vertexConfig struct { | |
| @@ -100,8 +102,7 @@ func loadVertexConfig() (*vertexConfig, error) { | |||
| 100 | 102 | credPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") | |
| 101 | 103 | if credPath == "" { | |
| 102 | 104 | // ADC default path. Mirrors `gcloud auth application-default login`. | |
| 103 | - | if home, err := os.UserHomeDir(); err == nil { | |
| 104 | - | candidate := home + "/.config/gcloud/application_default_credentials.json" | |
| 105 | + | if candidate, err := providerauth.GoogleApplicationDefaultCredentialsPath(); err == nil { | |
| 105 | 106 | if _, err := os.Stat(candidate); err == nil { | |
| 106 | 107 | credPath = candidate | |
| 107 | 108 | } | |
| @@ -51,6 +51,8 @@ import ( | |||||
| 51 | "strings" | 51 | "strings" | ||
| 52 | "sync" | 52 | "sync" | ||
| 53 | "time" | 53 | "time" | ||
| 54 | + | ||||
| 55 | + | providerauth "github.com/patriceckhart/zot/packages/provider/auth" | |||
| 54 | ) | 56 | ) | ||
| 55 | 57 | ||||
| 56 | type vertexConfig struct { | 58 | type vertexConfig struct { | ||
| @@ -100,8 +102,7 @@ func loadVertexConfig() (*vertexConfig, error) { | |||||
| 100 | credPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") | 102 | credPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") | ||
| 101 | if credPath == "" { | 103 | if credPath == "" { | ||
| 102 | // ADC default path. Mirrors `gcloud auth application-default login`. | 104 | // ADC default path. Mirrors `gcloud auth application-default login`. | ||
| 103 | - | if home, err := os.UserHomeDir(); err == nil { | 105 | + | if candidate, err := providerauth.GoogleApplicationDefaultCredentialsPath(); err == nil { |
| 104 | - | candidate := home + "/.config/gcloud/application_default_credentials.json" | |||
| 105 | if _, err := os.Stat(candidate); err == nil { | 106 | if _, err := os.Stat(candidate); err == nil { | ||
| 106 | credPath = candidate | 107 | credPath = candidate | ||
| 107 | } | 108 | } | ||