Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: make ADC the default option for GCP authentication when using go-containerregistry #9456

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions pkg/skaffold/docker/authenticators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
}
Expand All @@ -97,13 +100,46 @@ 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
}
}

// 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")
}
67 changes: 56 additions & 11 deletions pkg/skaffold/docker/authenticators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ limitations under the License.
package docker

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"runtime"
"testing"
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Loading