detached efd311e

Merge pull request #79 from ekoeppen/gcp-auth

Add Google application credentials authentication
commit
efd311ea6d02d20a8c5517777cbd33ca66b78b92
author
Patric Eckhart · · 2026-07-14 12:01 UTC
committer
GitHub · · 2026-07-14 12:01 UTC
merge of
f2588cf + bcb7dc3 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.2.84
6 files · +235 -3
M README.md +29 -1
@@ -546,7 +546,7 @@ Credential lookup order for Google:
546546
547547 Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta/models` once and stores the key under `google` in `auth.json`.
548548
549-> **Auth model: API key only.** Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. Programmatic access requires either an AI Studio API key (this provider) or a Vertex AI / GCP service-account credential (not yet wired up in zot). The `/login subscription` step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end.
549+> **Auth model: API key only (this provider).** Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. The `/login subscription` step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end. If you need OAuth/service-account auth instead of an API key, use the `google-vertex` provider below.
550550
551551 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).
552552
@@ -554,6 +554,34 @@ Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also conf
554554
555555 You can add additional Gemini model IDs to `models.json` under the `google` provider.
556556
557+### Gemini Enterprise Agent Platform (formerly Google Vertex AI)
558+
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+
561+```bash
562+zot --provider google-vertex
563+```
564+
565+Required configuration (env vars, read at construction time):
566+
567+- `GOOGLE_CLOUD_PROJECT` — required, your GCP project id.
568+- `GOOGLE_CLOUD_LOCATION` — optional, defaults to `us-central1`.
569+
570+Credential lookup order for Vertex:
571+
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.
582+
583+If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.
584+
557585 ### Local models with ollama
558586
559587 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
@@ -546,7 +546,7 @@ Credential lookup order for Google:
546 546
547 Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta/models` once and stores the key under `google` in `auth.json`.547 Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta/models` once and stores the key under `google` in `auth.json`.
548 548
549-> **Auth model: API key only.** Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. Programmatic access requires either an AI Studio API key (this provider) or a Vertex AI / GCP service-account credential (not yet wired up in zot). The `/login subscription` step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end.549+> **Auth model: API key only (this provider).** Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. The `/login subscription` step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end. If you need OAuth/service-account auth instead of an API key, use the `google-vertex` provider below.
550 550
551 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).551 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).
552 552
@@ -554,6 +554,34 @@ 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+### Gemini Enterprise Agent Platform (formerly Google Vertex AI)
558+
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+
561+```bash
562+zot --provider google-vertex
563+```
564+
565+Required configuration (env vars, read at construction time):
566+
567+- `GOOGLE_CLOUD_PROJECT` — required, your GCP project id.
568+- `GOOGLE_CLOUD_LOCATION` — optional, defaults to `us-central1`.
569+
570+Credential lookup order for Vertex:
571+
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.
582+
583+If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.
584+
557 ### Local models with ollama585 ### Local models with ollama
558 586
559 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.587 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
M packages/agent/config.go +11 -0
@@ -376,6 +376,17 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s
376376 if v := os.Getenv("GOOGLE_CLOUD_API_KEY"); v != "" {
377377 return v, "apikey", "", nil
378378 }
379+ // Check for Application Default Credentials (gcloud auth application-default login)
380+ // or service account JSON, matching the actual NewVertex client behavior.
381+ if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" {
382+ return "<adc>", "apikey", "", nil
383+ }
384+ // Check for the platform-specific default ADC path that gcloud writes to.
385+ if adcPath, err := auth.GoogleApplicationDefaultCredentialsPath(); err == nil {
386+ if _, err := os.Stat(adcPath); err == nil {
387+ return "<adc>", "apikey", "", nil
388+ }
389+ }
379390 case "azure-openai-responses":
380391 if v := os.Getenv("AZURE_OPENAI_API_KEY"); v != "" {
381392 return v, "apikey", "", nil
@@ -376,6 +376,17 @@ func ResolveCredentialFull(provider, explicit string) (cred, method, accountID s
376 if v := os.Getenv("GOOGLE_CLOUD_API_KEY"); v != "" {376 if v := os.Getenv("GOOGLE_CLOUD_API_KEY"); v != "" {
377 return v, "apikey", "", nil377 return v, "apikey", "", nil
378 }378 }
379+ // Check for Application Default Credentials (gcloud auth application-default login)
380+ // or service account JSON, matching the actual NewVertex client behavior.
381+ if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" {
382+ return "<adc>", "apikey", "", nil
383+ }
384+ // Check for the platform-specific default ADC path that gcloud writes to.
385+ if adcPath, err := auth.GoogleApplicationDefaultCredentialsPath(); err == nil {
386+ if _, err := os.Stat(adcPath); err == nil {
387+ return "<adc>", "apikey", "", nil
388+ }
389+ }
379 case "azure-openai-responses":390 case "azure-openai-responses":
380 if v := os.Getenv("AZURE_OPENAI_API_KEY"); v != "" {391 if v := os.Getenv("AZURE_OPENAI_API_KEY"); v != "" {
381 return v, "apikey", "", nil392 return v, "apikey", "", nil
A packages/agent/config_gcp_test.go +139 -0
@@ -0,0 +1,139 @@
1+package agent
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ providerauth "github.com/patriceckhart/zot/packages/provider/auth"
9+)
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+
23+// TestResolveCredentialGoogleVertexAPIKey confirms that GOOGLE_CLOUD_API_KEY
24+// takes top priority for the google-vertex provider and is returned with
25+// method "apikey".
26+func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) {
27+ t.Setenv("ZOT_HOME", t.TempDir())
28+ isolateGoogleVertexADC(t)
29+ t.Setenv("GOOGLE_CLOUD_API_KEY", "gcp-key-123")
30+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
31+
32+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
33+ if err != nil {
34+ t.Fatalf("ResolveCredentialFull failed: %v", err)
35+ }
36+ if cred != "gcp-key-123" {
37+ t.Errorf("cred = %q, want %q", cred, "gcp-key-123")
38+ }
39+ if method != "apikey" {
40+ t.Errorf("method = %q, want apikey", method)
41+ }
42+}
43+
44+// TestResolveCredentialGoogleVertexApplicationCredentialsEnv confirms that
45+// when GOOGLE_APPLICATION_CREDENTIALS is set (service account JSON path),
46+// resolution returns the "<adc>" sentinel with method "apikey", matching
47+// the real NewVertex client's ADC-based auth flow.
48+func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) {
49+ t.Setenv("ZOT_HOME", t.TempDir())
50+ isolateGoogleVertexADC(t)
51+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
52+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/service-account.json")
53+
54+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
55+ if err != nil {
56+ t.Fatalf("ResolveCredentialFull failed: %v", err)
57+ }
58+ if cred != "<adc>" {
59+ t.Errorf("cred = %q, want <adc>", cred)
60+ }
61+ if method != "apikey" {
62+ t.Errorf("method = %q, want apikey", method)
63+ }
64+}
65+
66+// TestResolveCredentialGoogleVertexDefaultADCFile confirms that, absent any
67+// env vars, the presence of the platform-specific default gcloud ADC file is
68+// detected and resolves to the "<adc>" sentinel.
69+func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) {
70+ t.Setenv("ZOT_HOME", t.TempDir())
71+ adcPath := isolateGoogleVertexADC(t)
72+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
73+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
74+
75+ if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil {
76+ t.Fatal(err)
77+ }
78+ if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil {
79+ t.Fatal(err)
80+ }
81+
82+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
83+ if err != nil {
84+ t.Fatalf("ResolveCredentialFull failed: %v", err)
85+ }
86+ if cred != "<adc>" {
87+ t.Errorf("cred = %q, want <adc>", cred)
88+ }
89+ if method != "apikey" {
90+ t.Errorf("method = %q, want apikey", method)
91+ }
92+}
93+
94+// TestResolveCredentialGoogleVertexPrecedence confirms GOOGLE_CLOUD_API_KEY
95+// wins over GOOGLE_APPLICATION_CREDENTIALS and the default ADC file when
96+// multiple credential sources are present simultaneously.
97+func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) {
98+ t.Setenv("ZOT_HOME", t.TempDir())
99+ adcPath := isolateGoogleVertexADC(t)
100+ t.Setenv("GOOGLE_CLOUD_API_KEY", "explicit-key")
101+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/sa.json")
102+
103+ if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil {
104+ t.Fatal(err)
105+ }
106+ if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil {
107+ t.Fatal(err)
108+ }
109+
110+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
111+ if err != nil {
112+ t.Fatalf("ResolveCredentialFull failed: %v", err)
113+ }
114+ if cred != "explicit-key" {
115+ t.Errorf("cred = %q, want explicit-key (API key must win over ADC)", cred)
116+ }
117+ if method != "apikey" {
118+ t.Errorf("method = %q, want apikey", method)
119+ }
120+}
121+
122+// TestResolveCredentialGoogleVertexNoCredentials confirms that when none of
123+// the Vertex credential sources are present (no env vars, no ADC file, no
124+// auth.json entry), resolution fails with an error rather than silently
125+// returning an empty credential.
126+func TestResolveCredentialGoogleVertexNoCredentials(t *testing.T) {
127+ t.Setenv("ZOT_HOME", t.TempDir())
128+ isolateGoogleVertexADC(t)
129+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
130+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
131+
132+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
133+ if err == nil {
134+ t.Fatalf("expected error, got cred=%q method=%q", cred, method)
135+ }
136+ if cred != "" || method != "" {
137+ t.Errorf("expected empty cred/method on error, got cred=%q method=%q", cred, method)
138+ }
139+}
@@ -0,0 +1,139 @@
1+package agent
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ providerauth "github.com/patriceckhart/zot/packages/provider/auth"
9+)
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+
23+// TestResolveCredentialGoogleVertexAPIKey confirms that GOOGLE_CLOUD_API_KEY
24+// takes top priority for the google-vertex provider and is returned with
25+// method "apikey".
26+func TestResolveCredentialGoogleVertexAPIKey(t *testing.T) {
27+ t.Setenv("ZOT_HOME", t.TempDir())
28+ isolateGoogleVertexADC(t)
29+ t.Setenv("GOOGLE_CLOUD_API_KEY", "gcp-key-123")
30+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
31+
32+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
33+ if err != nil {
34+ t.Fatalf("ResolveCredentialFull failed: %v", err)
35+ }
36+ if cred != "gcp-key-123" {
37+ t.Errorf("cred = %q, want %q", cred, "gcp-key-123")
38+ }
39+ if method != "apikey" {
40+ t.Errorf("method = %q, want apikey", method)
41+ }
42+}
43+
44+// TestResolveCredentialGoogleVertexApplicationCredentialsEnv confirms that
45+// when GOOGLE_APPLICATION_CREDENTIALS is set (service account JSON path),
46+// resolution returns the "<adc>" sentinel with method "apikey", matching
47+// the real NewVertex client's ADC-based auth flow.
48+func TestResolveCredentialGoogleVertexApplicationCredentialsEnv(t *testing.T) {
49+ t.Setenv("ZOT_HOME", t.TempDir())
50+ isolateGoogleVertexADC(t)
51+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
52+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/service-account.json")
53+
54+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
55+ if err != nil {
56+ t.Fatalf("ResolveCredentialFull failed: %v", err)
57+ }
58+ if cred != "<adc>" {
59+ t.Errorf("cred = %q, want <adc>", cred)
60+ }
61+ if method != "apikey" {
62+ t.Errorf("method = %q, want apikey", method)
63+ }
64+}
65+
66+// TestResolveCredentialGoogleVertexDefaultADCFile confirms that, absent any
67+// env vars, the presence of the platform-specific default gcloud ADC file is
68+// detected and resolves to the "<adc>" sentinel.
69+func TestResolveCredentialGoogleVertexDefaultADCFile(t *testing.T) {
70+ t.Setenv("ZOT_HOME", t.TempDir())
71+ adcPath := isolateGoogleVertexADC(t)
72+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
73+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
74+
75+ if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil {
76+ t.Fatal(err)
77+ }
78+ if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil {
79+ t.Fatal(err)
80+ }
81+
82+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
83+ if err != nil {
84+ t.Fatalf("ResolveCredentialFull failed: %v", err)
85+ }
86+ if cred != "<adc>" {
87+ t.Errorf("cred = %q, want <adc>", cred)
88+ }
89+ if method != "apikey" {
90+ t.Errorf("method = %q, want apikey", method)
91+ }
92+}
93+
94+// TestResolveCredentialGoogleVertexPrecedence confirms GOOGLE_CLOUD_API_KEY
95+// wins over GOOGLE_APPLICATION_CREDENTIALS and the default ADC file when
96+// multiple credential sources are present simultaneously.
97+func TestResolveCredentialGoogleVertexPrecedence(t *testing.T) {
98+ t.Setenv("ZOT_HOME", t.TempDir())
99+ adcPath := isolateGoogleVertexADC(t)
100+ t.Setenv("GOOGLE_CLOUD_API_KEY", "explicit-key")
101+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/sa.json")
102+
103+ if err := os.MkdirAll(filepath.Dir(adcPath), 0o755); err != nil {
104+ t.Fatal(err)
105+ }
106+ if err := os.WriteFile(adcPath, []byte(`{"type":"authorized_user"}`), 0o600); err != nil {
107+ t.Fatal(err)
108+ }
109+
110+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
111+ if err != nil {
112+ t.Fatalf("ResolveCredentialFull failed: %v", err)
113+ }
114+ if cred != "explicit-key" {
115+ t.Errorf("cred = %q, want explicit-key (API key must win over ADC)", cred)
116+ }
117+ if method != "apikey" {
118+ t.Errorf("method = %q, want apikey", method)
119+ }
120+}
121+
122+// TestResolveCredentialGoogleVertexNoCredentials confirms that when none of
123+// the Vertex credential sources are present (no env vars, no ADC file, no
124+// auth.json entry), resolution fails with an error rather than silently
125+// returning an empty credential.
126+func TestResolveCredentialGoogleVertexNoCredentials(t *testing.T) {
127+ t.Setenv("ZOT_HOME", t.TempDir())
128+ isolateGoogleVertexADC(t)
129+ t.Setenv("GOOGLE_CLOUD_API_KEY", "")
130+ t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
131+
132+ cred, method, _, err := ResolveCredentialFull("google-vertex", "")
133+ if err == nil {
134+ t.Fatalf("expected error, got cred=%q method=%q", cred, method)
135+ }
136+ if cred != "" || method != "" {
137+ t.Errorf("expected empty cred/method on error, got cred=%q method=%q", cred, method)
138+ }
139+}
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 (
5151 "strings"
5252 "sync"
5353 "time"
54+
55+ providerauth "github.com/patriceckhart/zot/packages/provider/auth"
5456 )
5557
5658 type vertexConfig struct {
@@ -100,8 +102,7 @@ func loadVertexConfig() (*vertexConfig, error) {
100102 credPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
101103 if credPath == "" {
102104 // 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 {
105106 if _, err := os.Stat(candidate); err == nil {
106107 credPath = candidate
107108 }
@@ -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 = candidate107 credPath = candidate
107 }108 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close