diff --git a/pkg/skaffold/docker/authenticators.go b/pkg/skaffold/docker/authenticators.go index e077e8d21f9..17b3d996280 100644 --- a/pkg/skaffold/docker/authenticators.go +++ b/pkg/skaffold/docker/authenticators.go @@ -17,12 +17,15 @@ limitations under the License. package docker import ( + "context" "strings" "sync" "github.com/docker/cli/cli/config" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/google" + + "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log" ) var primaryKeychain = &Keychain{ @@ -76,17 +79,17 @@ func (a *lockedAuthenticator) Authorization() (*authn.AuthConfig, error) { } // Create a new authenticator for a given reference -// 1. If `gcloud` is configured, we use google.NewGcloudAuthenticator(). It is more efficient because it reuses tokens. +// 1. If `gcloud` is configured with given registry, we try to use a Google authenticator // 2. If something else is configured, we use that authenticator // 3. If nothing is configured, we check if `gcloud` can be used // 4. Default to anonymous func (a *Keychain) newAuthenticator(res authn.Resource) authn.Authenticator { registry := res.RegistryStr() - // 1. Use google.NewGcloudAuthenticator() authenticator if `gcloud` is configured + // 1. Try getting a Google authenticator if docker config configured to use gcloud cfg, err := config.Load(a.configDir) if err == nil && cfg.CredentialHelpers[registry] == "gcloud" { - if auth, err := google.NewGcloudAuthenticator(); err == nil { + if auth := getGoogleAuthenticator(); auth != nil { return auth } } @@ -97,9 +100,9 @@ func (a *Keychain) newAuthenticator(res authn.Resource) authn.Authenticator { return auth } - // 3. Try gcloud for *.gcr.io - if registry == "gcr.io" || strings.HasSuffix(registry, ".gcr.io") { - if auth, err := google.NewGcloudAuthenticator(); err == nil { + // 3. Try Google authenticator for known registries (same logic used by go-containerregistry) + if isGoogleRegistry(registry) { + if auth := getGoogleAuthenticator(); auth != nil { return auth } } @@ -107,3 +110,36 @@ func (a *Keychain) newAuthenticator(res authn.Resource) authn.Authenticator { // 4. Default to anonymous return authn.Anonymous } + +func getGoogleAuthenticator() authn.Authenticator { + // 1. First we try to create an authenticator that uses Application Default Credentials + auth, err := google.NewEnvAuthenticator() + if err == nil && auth != authn.Anonymous { + log.Entry(context.TODO()).Debugf("using Application Default Credentials authenticator") + return auth + } + + if err != nil { + log.Entry(context.TODO()).Debugf("failed to get Application Default Credentials auth: %v", err) + } + + // 2. Try to create authenticator that uses gcloud + auth, err = google.NewGcloudAuthenticator() + if err == nil && auth != authn.Anonymous { + log.Entry(context.TODO()).Debugf("using gcloud authenticator") + return auth + } + + if err != nil { + log.Entry(context.TODO()).Debugf("failed to get gcloud auth: %v", err) + } + + return nil +} + +func isGoogleRegistry(host string) bool { + return host == "gcr.io" || + strings.HasSuffix(host, ".gcr.io") || + strings.HasSuffix(host, ".pkg.dev") || + strings.HasSuffix(host, ".google.com") +} diff --git a/pkg/skaffold/docker/authenticators_test.go b/pkg/skaffold/docker/authenticators_test.go index 4537af62163..164df8fa766 100644 --- a/pkg/skaffold/docker/authenticators_test.go +++ b/pkg/skaffold/docker/authenticators_test.go @@ -17,6 +17,9 @@ limitations under the License. package docker import ( + "encoding/json" + "net/http" + "net/http/httptest" "os" "runtime" "testing" @@ -34,13 +37,28 @@ func TestResolve(t *testing.T) { } tests := []struct { - description string - dockerConfig string - registry string - gcloudOutput string - gcloudInPath bool - expectAnonymous bool + description string + dockerConfig string + registry string + gcloudOutput string + credentialsValues map[string]string + tokenURIRequestOutput string + gcloudInPath bool + expectAnonymous bool }{ + { + description: "Application Default Credentials configured and working", + registry: "gcr.io", + dockerConfig: `{"credHelpers":{"anydomain.io": "gcloud"}}`, + credentialsValues: map[string]string{ + "client_id": "123456.apps.googleusercontent.com", + "client_secret": "THE-SECRET", + "refresh_token": "REFRESH-TOKEN", + "type": "authorized_user", + }, + tokenURIRequestOutput: `{"access_token":"TOKEN","expires_in": 3599}`, + expectAnonymous: false, + }, { description: "gcloud is configured and working", registry: "gcr.io", @@ -91,17 +109,25 @@ func TestResolve(t *testing.T) { testutil.Run(t, test.description, func(t *testutil.T) { tmpDir := t.NewTempDir().Write("config.json", test.dockerConfig) - var path string + var path = tmpDir.Root() if test.gcloudInPath { path = tmpDir.Root() + ":" + os.Getenv("PATH") tmpDir.Write("gcloud", test.gcloudOutput) - } else { - path = tmpDir.Root() + } + + var adc string + if test.credentialsValues != nil { + url := startTokenServer(t, test.tokenURIRequestOutput) + credentialsFile := getCredentialsFile(t, test.credentialsValues, url) + tmpDir.Write("credentials.json", credentialsFile) + adc = tmpDir.Path("credentials.json") } t.SetEnvs(map[string]string{ - "DOCKER_CONFIG": tmpDir.Path("config.json"), - "PATH": path, + "DOCKER_CONFIG": tmpDir.Path("config.json"), + "PATH": path, + "HOME": tmpDir.Root(), // This is to prevent the go-containerregistry library from using ADCs that are already present on the computer. + "GOOGLE_APPLICATION_CREDENTIALS": adc, }) registry, err := name.NewRegistry(test.registry) @@ -122,3 +148,22 @@ func TestResolve(t *testing.T) { }) } } + +func startTokenServer(t *testutil.T, reqOutput string) string { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(reqOutput)) + })) + t.Cleanup(server.Close) + return server.URL +} + +func getCredentialsFile(t *testutil.T, credValues map[string]string, tokenRefreshURL string) string { + credValues["token_uri"] = tokenRefreshURL + credFile, err := json.Marshal(credValues) + if err != nil { + t.Fatalf("error generating credential files: %v", err) + } + return string(credFile) +}