From 1755dfc704a0402d7a5de374e06f0b53ca3e3a06 Mon Sep 17 00:00:00 2001 From: Renzo Rojas Silva Date: Sun, 7 Jul 2024 21:53:19 -0400 Subject: [PATCH] feat: default to ADC when `gcloud` cred helper is configured in docker/config.json when using docker go library --- pkg/skaffold/docker/auth.go | 90 +++++++++++++++++++++++++++----- pkg/skaffold/docker/auth_test.go | 52 ++++++++++++++++-- 2 files changed, 125 insertions(+), 17 deletions(-) diff --git a/pkg/skaffold/docker/auth.go b/pkg/skaffold/docker/auth.go index 70c41b9e157..8f319464235 100644 --- a/pkg/skaffold/docker/auth.go +++ b/pkg/skaffold/docker/auth.go @@ -18,18 +18,20 @@ package docker import ( "context" - "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" + "github.com/distribution/reference" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" - "github.com/docker/distribution/reference" + clitypes "github.com/docker/cli/cli/config/types" types "github.com/docker/docker/api/types/registry" "github.com/docker/docker/pkg/homedir" "github.com/docker/docker/registry" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/google" "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/gcp" "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log" @@ -79,14 +81,72 @@ func (h credsHelper) GetAuthConfig(registry string) (types.AuthConfig, error) { return types.AuthConfig{}, err } + return h.loadCredentials(cf, registry) +} + +func (h credsHelper) loadCredentials(cf *configfile.ConfigFile, registry string) (types.AuthConfig, error) { + if helper := cf.CredentialHelpers[registry]; helper == "gcloud" { + authCfg, err := h.getGoogleAuthConfig(registry) + if err == nil { + return authCfg, nil + } + log.Entry(context.TODO()).Debugf("error getting google authenticator, falling back to docker auth: %v", err) + } + + var anonymous clitypes.AuthConfig auth, err := cf.GetAuthConfig(registry) if err != nil { return types.AuthConfig{}, err } + // From go-containerrergistry logic, the ServerAddress is not considered when determining if returned auth is anonymous. + anonymous.ServerAddress = auth.ServerAddress + if auth != anonymous { + return types.AuthConfig(auth), nil + } + + if isGoogleRegistry(registry) { + authCfg, err := h.getGoogleAuthConfig(registry) + if err == nil { + return authCfg, nil + } + } + return types.AuthConfig(auth), nil } +func (h credsHelper) getGoogleAuthConfig(registry string) (types.AuthConfig, error) { + auth, err := google.NewEnvAuthenticator() + if err != nil { + return types.AuthConfig{}, err + } + + if auth == authn.Anonymous { + return types.AuthConfig{}, fmt.Errorf("error getting google authenticator") + } + + cfg, err := auth.Authorization() + if err != nil { + return types.AuthConfig{}, err + } + + bCfg, err := cfg.MarshalJSON() + if err != nil { + return types.AuthConfig{}, err + } + + var authCfg types.AuthConfig + err = json.Unmarshal(bCfg, &authCfg) + if err != nil { + return types.AuthConfig{}, err + } + + // The docker library does the same when we request the credentials + authCfg.ServerAddress = registry + + return authCfg, nil +} + // GetAllAuthConfigs retrieves all the auth configs. // Because this can take a long time, we make sure it can be interrupted by the user. func (h credsHelper) GetAllAuthConfigs(ctx context.Context) (map[string]types.AuthConfig, error) { @@ -111,22 +171,31 @@ func (h credsHelper) GetAllAuthConfigs(ctx context.Context) (map[string]types.Au } func (h credsHelper) doGetAllAuthConfigs() (map[string]types.AuthConfig, error) { + credentials := make(map[string]types.AuthConfig) cf, err := loadDockerConfig() if err != nil { return nil, err } - credentials, err := cf.GetAllCredentials() + defaultCreds, err := cf.GetCredentialsStore("").GetAll() if err != nil { return nil, err } - authConfigs := make(map[string]types.AuthConfig, len(credentials)) - for k, auth := range credentials { - authConfigs[k] = types.AuthConfig(auth) + for registry, cred := range defaultCreds { + credentials[registry] = types.AuthConfig(cred) + } + + for registry := range cf.CredentialHelpers { + authCfg, err := h.loadCredentials(cf, registry) + if err != nil { + log.Entry(context.TODO()).Debugf("failed to get credentials for registry %v: %v", registry, err) + continue + } + credentials[registry] = authCfg } - return authConfigs, nil + return credentials, nil } func (l *localDaemon) encodedRegistryAuth(ctx context.Context, a AuthConfigHelper, image string) (string, error) { @@ -150,12 +219,7 @@ func (l *localDaemon) encodedRegistryAuth(ctx context.Context, a AuthConfigHelpe return "", fmt.Errorf("getting auth config: %w", err) } - buf, err := json.Marshal(ac) - if err != nil { - return "", err - } - - return base64.URLEncoding.EncodeToString(buf), nil + return types.EncodeAuthConfig(ac) } func (l *localDaemon) officialRegistry(ctx context.Context) string { diff --git a/pkg/skaffold/docker/auth_test.go b/pkg/skaffold/docker/auth_test.go index 32a0103ab36..99ed7e21e4a 100644 --- a/pkg/skaffold/docker/auth_test.go +++ b/pkg/skaffold/docker/auth_test.go @@ -58,14 +58,18 @@ func TestGetAllAuthConfigs(t *testing.T) { tmpDir := t.NewTempDir(). Write("config.json", `{"credHelpers":{"my.registry":"helper"}}`). Write("docker-credential-gcloud", `#!/bin/sh -read server -echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`). + read server + echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`). Write("docker-credential-helper", `#!/bin/sh -read server -echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`) + read server + echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`) t.Override(&configDir, tmpDir.Root()) t.Setenv("PATH", tmpDir.Root()) + // These env values will prevent the authenticator to use the Google authenticator, it will use docker logic instead. + t.Setenv("HOME", tmpDir.Root()) + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") + auth, err := DefaultAuthHelper.GetAllAuthConfigs(context.Background()) t.CheckNoError(err) @@ -89,6 +93,46 @@ echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`) t.CheckError(true, err) t.CheckEmpty(auth) }) + + testutil.Run(t, "Application Default Credentials authentication", func(t *testutil.T) { + if runtime.GOOS == "windows" { + t.Skip("test doesn't work on windows") + } + + authToken := `{"access_token":"TOKEN","expires_in": 3599}` + authServerURL := startTokenServer(t, authToken) + credentialsFile := getCredentialsFile(t, map[string]string{ + "client_id": "123456.apps.googleusercontent.com", + "client_secret": "THE-SECRET", + "refresh_token": "REFRESH-TOKEN", + "type": "authorized_user", + }, authServerURL) + + tmpDir := t.NewTempDir().Write("credentials.json", credentialsFile) + tmpDir.Write("config.json", `{"credHelpers":{"my.registry1":"helper","my.registry2":"missinghelper","gcr.io":"gcloud","us-central1-docker.pkg.dev":"otherhelper","us-east1-docker.pkg.dev":"gcloud"}}`) + tmpDir.Write("docker-credential-helper", `#!/bin/sh + read server + echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`) + tmpDir.Write("docker-credential-otherhelper", `#!/bin/sh + read server + echo "{\"Username\":\"\",\"Secret\":\"TOKEN_$server\"}"`) + + t.Override(&configDir, tmpDir.Root()) + t.SetEnvs(map[string]string{ + "PATH": tmpDir.Root(), + "HOME": tmpDir.Root(), // This is to prevent the go-containerregistry library from using ADCs that are already present on the computer. + "GOOGLE_APPLICATION_CREDENTIALS": tmpDir.Path("credentials.json"), + }) + + auth, err := DefaultAuthHelper.GetAllAuthConfigs(context.Background()) + t.CheckNoError(err) + t.CheckDeepEqual(map[string]registry.AuthConfig{ + "gcr.io": {Username: "_token", Password: "TOKEN", Auth: "X3Rva2VuOlRPS0VO", ServerAddress: "gcr.io"}, + "us-east1-docker.pkg.dev": {Username: "_token", Password: "TOKEN", Auth: "X3Rva2VuOlRPS0VO", ServerAddress: "us-east1-docker.pkg.dev"}, + "us-central1-docker.pkg.dev": {IdentityToken: "TOKEN_us-central1-docker.pkg.dev", ServerAddress: "us-central1-docker.pkg.dev"}, + "my.registry1": {IdentityToken: "TOKEN_my.registry1", ServerAddress: "my.registry1"}, + }, auth) + }) } func TestGetEncodedRegistryAuth(t *testing.T) {