diff --git a/cmd/warmer/cmd/root.go b/cmd/warmer/cmd/root.go index 61333daeec..e60caa5f92 100644 --- a/cmd/warmer/cmd/root.go +++ b/cmd/warmer/cmd/root.go @@ -19,13 +19,11 @@ package cmd import ( "fmt" "os" - "strings" "time" "github.com/GoogleContainerTools/kaniko/pkg/cache" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/logging" - "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -65,20 +63,6 @@ var RootCmd = &cobra.Command{ exit(errors.Wrap(err, "Failed to create cache directory")) } } - isGCR := false - for _, image := range opts.Images { - if strings.Contains(image, "gcr.io") || strings.Contains(image, ".pkg.dev") { - isGCR = true - break - } - } - // Historically kaniko was pre-configured by default with gcr credential helper, - // in here we keep the backwards compatibility by enabling the GCR helper only - // when gcr.io (or pkg.dev) is in one of the destinations. - if isGCR { - util.ConfigureGCR("") - } - if err := cache.WarmCache(opts); err != nil { exit(errors.Wrap(err, "Failed warming cache")) } diff --git a/pkg/creds/creds_darwin.go b/pkg/creds/creds_darwin.go index e3bdb2c946..b0c9672e46 100644 --- a/pkg/creds/creds_darwin.go +++ b/pkg/creds/creds_darwin.go @@ -17,20 +17,33 @@ limitations under the License. package creds import ( + "log" "sync" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/google" ) var ( - setupKeyChainOnce sync.Once - keyChain authn.Keychain + setupKeychainOnce sync.Once + keychain authn.Keychain ) // GetKeychain returns a keychain for accessing container registries. func GetKeychain() authn.Keychain { - setupKeyChainOnce.Do(func() { - keyChain = authn.NewMultiKeychain(authn.DefaultKeychain) + setupKeychainOnce.Do(func() { + keychain = authn.DefaultKeychain + + // Historically kaniko was pre-configured by default with gcr + // credential helper, in here we keep the backwards + // compatibility by enabling the GCR helper only when gcr.io + // (or pkg.dev) is in one of the destinations. + gauth, err := google.NewEnvAuthenticator() + if err != nil { + log.Printf("Failed to setup Google env authenticator, ignoring: %v", err) + } else { + keychain = authn.NewMultiKeychain(authn.DefaultKeychain, gcrKeychain{gauth}) + } }) - return keyChain + return keychain } diff --git a/pkg/creds/creds_linux.go b/pkg/creds/creds_linux.go index 2cbe62682f..ee1ce1cd21 100644 --- a/pkg/creds/creds_linux.go +++ b/pkg/creds/creds_linux.go @@ -23,18 +23,30 @@ import ( "github.com/genuinetools/bpfd/proc" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/v1/google" "github.com/sirupsen/logrus" ) var ( - setupKeyChainOnce sync.Once - keyChain authn.Keychain + setupKeychainOnce sync.Once + keychain authn.Keychain ) // GetKeychain returns a keychain for accessing container registries. func GetKeychain() authn.Keychain { - setupKeyChainOnce.Do(func() { - keyChain = authn.NewMultiKeychain(authn.DefaultKeychain) + setupKeychainOnce.Do(func() { + keychain = authn.DefaultKeychain + + // Historically kaniko was pre-configured by default with gcr + // credential helper, in here we keep the backwards + // compatibility by enabling the GCR helper only when gcr.io + // (or pkg.dev) is in one of the destinations. + gauth, err := google.NewEnvAuthenticator() + if err != nil { + logrus.Warnf("Failed to setup Google env authenticator, ignoring: %v", err) + } else { + keychain = authn.NewMultiKeychain(authn.DefaultKeychain, gcrKeychain{gauth}) + } // Add the Kubernetes keychain if we're on Kubernetes if proc.GetContainerRuntime(0, 0) == proc.RuntimeKubernetes { @@ -43,8 +55,8 @@ func GetKeychain() authn.Keychain { logrus.Warnf("Error setting up k8schain. Using default keychain %s", err) return } - keyChain = authn.NewMultiKeychain(keyChain, k8sc) + keychain = authn.NewMultiKeychain(keychain, k8sc) } }) - return keyChain + return keychain } diff --git a/pkg/creds/gcr_keychain.go b/pkg/creds/gcr_keychain.go new file mode 100644 index 0000000000..5129be50a0 --- /dev/null +++ b/pkg/creds/gcr_keychain.go @@ -0,0 +1,37 @@ +/* +Copyright 2021 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package creds + +import ( + "strings" + + "github.com/google/go-containerregistry/pkg/authn" +) + +type gcrKeychain struct { + authr authn.Authenticator +} + +func (g gcrKeychain) Resolve(r authn.Resource) (authn.Authenticator, error) { + if r.RegistryStr() == "gcr.io" || + strings.HasSuffix(r.RegistryStr(), ".gcr.io") || + strings.HasSuffix(r.RegistryStr(), ".pkg.dev") { + + return g.authr, nil + } + return authn.Anonymous, nil +} diff --git a/pkg/executor/push.go b/pkg/executor/push.go index e4fe030022..9f75305e5d 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -95,14 +95,6 @@ func CheckPushPermissions(opts *config.KanikoOptions) error { } registryName := destRef.Repository.Registry.Name() - // Historically kaniko was pre-configured by default with gcr credential helper, - // in here we keep the backwards compatibility by enabling the GCR helper only - // when gcr.io (or pkg.dev) is in one of the destinations. - if registryName == "gcr.io" || strings.HasSuffix(registryName, ".gcr.io") || strings.HasSuffix(registryName, ".pkg.dev") { - if err := util.ConfigureGCR(fmt.Sprintf("--registries=%s", registryName)); err != nil { - return err - } - } if opts.Insecure || opts.InsecureRegistries.Contains(registryName) { newReg, err := name.NewRegistry(registryName, name.WeakValidation, name.Insecure) if err != nil { diff --git a/pkg/util/gcr_util.go b/pkg/util/gcr_util.go index 7627ac6de4..ac7e946858 100644 --- a/pkg/util/gcr_util.go +++ b/pkg/util/gcr_util.go @@ -17,15 +17,8 @@ limitations under the License. package util import ( - "bytes" - "fmt" "os" - "os/exec" "path/filepath" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spf13/afero" ) // DockerConfLocation returns the file system location of the Docker @@ -49,21 +42,3 @@ func DockerConfLocation() string { } return string(os.PathSeparator) + filepath.Join("kaniko", ".docker", configFile) } - -func ConfigureGCR(flags string) error { - // Checking for existence of docker.config as it's normally required for - // authenticated registries and prevent overwriting user provided docker conf - _, err := afero.NewOsFs().Stat(DockerConfLocation()) - dockerConfNotExists := os.IsNotExist(err) - if dockerConfNotExists { - cmd := exec.Command("docker-credential-gcr", "configure-docker", flags) - var out bytes.Buffer - cmd.Stderr = &out - if err := cmd.Run(); err != nil { - return errors.Wrap(err, fmt.Sprintf("error while configuring docker-credential-gcr helper: %s : %s", cmd.String(), out.String())) - } - } else { - logrus.Warnf("\nSkip running docker-credential-gcr as user provided docker configuration exists at %s", DockerConfLocation()) - } - return nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/README.md b/vendor/github.com/google/go-containerregistry/pkg/v1/google/README.md new file mode 100644 index 0000000000..7cd8971fe5 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/README.md @@ -0,0 +1,7 @@ +# `google` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google) + +The `google` package provides: +* Some google-specific authentication methods. +* Some [GCR](gcr.io)-specific listing methods. diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/auth.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/auth.go new file mode 100644 index 0000000000..15b74e989a --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/auth.go @@ -0,0 +1,180 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "golang.org/x/oauth2" + googauth "golang.org/x/oauth2/google" +) + +const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" + +// GetGcloudCmd is exposed so we can test this. +var GetGcloudCmd = func() *exec.Cmd { + // This is odd, but basically what docker-credential-gcr does. + // + // config-helper is undocumented, but it's purportedly the only supported way + // of accessing tokens (`gcloud auth print-access-token` is discouraged). + // + // --force-auth-refresh means we are getting a token that is valid for about + // an hour (we reuse it until it's expired). + return exec.Command("gcloud", "config", "config-helper", "--force-auth-refresh", "--format=json(credential)") +} + +// NewEnvAuthenticator returns an authn.Authenticator that generates access +// tokens from the environment we're running in. +// +// See: https://godoc.org/golang.org/x/oauth2/google#FindDefaultCredentials +func NewEnvAuthenticator() (authn.Authenticator, error) { + ts, err := googauth.DefaultTokenSource(context.Background(), cloudPlatformScope) + if err != nil { + return nil, err + } + + token, err := ts.Token() + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil +} + +// NewGcloudAuthenticator returns an oauth2.TokenSource that generates access +// tokens by shelling out to the gcloud sdk. +func NewGcloudAuthenticator() (authn.Authenticator, error) { + if _, err := exec.LookPath("gcloud"); err != nil { + // gcloud is not available, fall back to anonymous + logs.Warn.Println("gcloud binary not found") + return authn.Anonymous, nil + } + + ts := gcloudSource{GetGcloudCmd()} + + // Attempt to fetch a token to ensure gcloud is installed and we can run it. + token, err := ts.Token() + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil +} + +// NewJSONKeyAuthenticator returns a Basic authenticator which uses Service Account +// as a way of authenticating with Google Container Registry. +// More information: https://cloud.google.com/container-registry/docs/advanced-authentication#json_key_file +func NewJSONKeyAuthenticator(serviceAccountJSON string) authn.Authenticator { + return &authn.Basic{ + Username: "_json_key", + Password: serviceAccountJSON, + } +} + +// NewTokenAuthenticator returns an oauth2.TokenSource that generates access +// tokens by using the Google SDK to produce JWT tokens from a Service Account. +// More information: https://godoc.org/golang.org/x/oauth2/google#JWTAccessTokenSourceFromJSON +func NewTokenAuthenticator(serviceAccountJSON string, scope string) (authn.Authenticator, error) { + ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), string(scope)) + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(nil, ts)}, nil +} + +// NewTokenSourceAuthenticator converts an oauth2.TokenSource into an authn.Authenticator. +func NewTokenSourceAuthenticator(ts oauth2.TokenSource) authn.Authenticator { + return &tokenSourceAuth{ts} +} + +// tokenSourceAuth turns an oauth2.TokenSource into an authn.Authenticator. +type tokenSourceAuth struct { + oauth2.TokenSource +} + +// Authorization implements authn.Authenticator. +func (tsa *tokenSourceAuth) Authorization() (*authn.AuthConfig, error) { + token, err := tsa.Token() + if err != nil { + return nil, err + } + + return &authn.AuthConfig{ + Username: "_token", + Password: token.AccessToken, + }, nil +} + +// gcloudOutput represents the output of the gcloud command we invoke. +// +// `gcloud config config-helper --format=json(credential)` looks something like: +// +// { +// "credential": { +// "access_token": "ya29.abunchofnonsense", +// "token_expiry": "2018-12-02T04:08:13Z" +// } +// } +type gcloudOutput struct { + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + } `json:"credential"` +} + +type gcloudSource struct { + // This is passed in so that we mock out gcloud and test Token. + cmd *exec.Cmd +} + +// Token implements oauath2.TokenSource. +func (gs gcloudSource) Token() (*oauth2.Token, error) { + cmd := gs.cmd + var out bytes.Buffer + cmd.Stdout = &out + + // Don't attempt to interpret stderr, just pass it through. + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("error executing `gcloud config config-helper`: %v", err) + } + + creds := gcloudOutput{} + if err := json.Unmarshal(out.Bytes(), &creds); err != nil { + return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %v", err) + } + + expiry, err := time.Parse(time.RFC3339, creds.Credential.TokenExpiry) + if err != nil { + return nil, fmt.Errorf("failed to parse gcloud token expiry: %v", err) + } + + token := oauth2.Token{ + AccessToken: creds.Credential.AccessToken, + Expiry: expiry, + } + + return &token, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/doc.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/doc.go new file mode 100644 index 0000000000..b6a67df049 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package google provides facilities for listing images in gcr.io. +package google diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/keychain.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/keychain.go new file mode 100644 index 0000000000..a69dba96d6 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/keychain.go @@ -0,0 +1,81 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "fmt" + "strings" + "sync" + + "github.com/google/go-containerregistry/pkg/authn" +) + +// Keychain exports an instance of the google Keychain. +var Keychain authn.Keychain = &googleKeychain{} + +type googleKeychain struct { + once sync.Once + auth authn.Authenticator + err error +} + +// Resolve implements authn.Keychain a la docker-credential-gcr. +// +// This behaves similarly to the GCR credential helper, but reuses tokens until +// they expire. +// +// We can't easily add this behavior to our credential helper implementation +// of authn.Authenticator because the credential helper protocol doesn't include +// expiration information, see here: +// https://godoc.org/github.com/docker/docker-credential-helpers/credentials#Credentials +// +// In addition to being a performance optimization, the reuse of these access +// tokens works around a bug in gcloud. It appears that attempting to invoke +// `gcloud config config-helper` multiple times too quickly will fail: +// https://github.com/GoogleCloudPlatform/docker-credential-gcr/issues/54 +// +// We could upstream this behavior into docker-credential-gcr by parsing +// gcloud's output and persisting its tokens across invocations, but then +// we have to deal with invalidating caches across multiple runs (no fun). +// +// In general, we don't worry about that here because we expect to use the same +// gcloud configuration in the scope of this one process. +func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { + // Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback. + host := target.RegistryStr() + if host != "gcr.io" && !strings.HasSuffix(host, ".gcr.io") && !strings.HasSuffix(host, ".pkg.dev") { + return authn.Anonymous, nil + } + + gk.once.Do(func() { + gk.auth, gk.err = resolve() + }) + + return gk.auth, gk.err +} + +func resolve() (authn.Authenticator, error) { + auth, envErr := NewEnvAuthenticator() + if envErr == nil { + return auth, nil + } + + auth, gErr := NewGcloudAuthenticator() + if gErr == nil { + return auth, nil + } + + return nil, fmt.Errorf("failed to create token source from env: %v or gcloud: %v", envErr, gErr) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go new file mode 100644 index 0000000000..4ca99b169d --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/list.go @@ -0,0 +1,259 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// ListerOption is a functional option for List and Walk. +// TODO: Can we somehow reuse the remote options here? +type ListerOption func(*lister) error + +type lister struct { + auth authn.Authenticator + transport http.RoundTripper + repo name.Repository + client *http.Client + ctx context.Context + userAgent string +} + +func newLister(repo name.Repository, options ...ListerOption) (*lister, error) { + l := &lister{ + auth: authn.Anonymous, + transport: http.DefaultTransport, + repo: repo, + ctx: context.Background(), + } + + for _, option := range options { + if err := option(l); err != nil { + return nil, err + } + } + + // Wrap the transport in something that logs requests and responses. + // It's expensive to generate the dumps, so skip it if we're writing + // to nothing. + if logs.Enabled(logs.Debug) { + l.transport = transport.NewLogger(l.transport) + } + + // Wrap the transport in something that can retry network flakes. + l.transport = transport.NewRetry(l.transport) + + // Wrap this last to prevent transport.New from double-wrapping. + if l.userAgent != "" { + l.transport = transport.NewUserAgent(l.transport, l.userAgent) + } + + scopes := []string{repo.Scope(transport.PullScope)} + tr, err := transport.NewWithContext(l.ctx, repo.Registry, l.auth, l.transport, scopes) + if err != nil { + return nil, err + } + + l.client = &http.Client{Transport: tr} + + return l, nil +} + +func (l *lister) list(repo name.Repository) (*Tags, error) { + uri := url.URL{ + Scheme: repo.Registry.Scheme(), + Host: repo.Registry.RegistryStr(), + Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()), + } + + req, err := http.NewRequestWithContext(l.ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, err + } + resp, err := l.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + tags := Tags{} + if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { + return nil, err + } + + return &tags, nil +} + +type rawManifestInfo struct { + Size string `json:"imageSizeBytes"` + MediaType string `json:"mediaType"` + Created string `json:"timeCreatedMs"` + Uploaded string `json:"timeUploadedMs"` + Tags []string `json:"tag"` +} + +// ManifestInfo is a Manifests entry is the output of List and Walk. +type ManifestInfo struct { + Size uint64 `json:"imageSizeBytes"` + MediaType string `json:"mediaType"` + Created time.Time `json:"timeCreatedMs"` + Uploaded time.Time `json:"timeUploadedMs"` + Tags []string `json:"tag"` +} + +func fromUnixMs(ms int64) time.Time { + sec := ms / 1000 + ns := (ms % 1000) * 1000000 + return time.Unix(sec, ns) +} + +func toUnixMs(t time.Time) string { + return strconv.FormatInt(t.UnixNano()/1000000, 10) +} + +// MarshalJSON implements json.Marshaler +func (m ManifestInfo) MarshalJSON() ([]byte, error) { + return json.Marshal(rawManifestInfo{ + Size: strconv.FormatUint(m.Size, 10), + MediaType: m.MediaType, + Created: toUnixMs(m.Created), + Uploaded: toUnixMs(m.Uploaded), + Tags: m.Tags, + }) +} + +// UnmarshalJSON implements json.Unmarshaler +func (m *ManifestInfo) UnmarshalJSON(data []byte) error { + raw := rawManifestInfo{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if raw.Size != "" { + size, err := strconv.ParseUint(string(raw.Size), 10, 64) + if err != nil { + return err + } + m.Size = size + } + + if raw.Created != "" { + created, err := strconv.ParseInt(string(raw.Created), 10, 64) + if err != nil { + return err + } + m.Created = fromUnixMs(created) + } + + if raw.Uploaded != "" { + uploaded, err := strconv.ParseInt(string(raw.Uploaded), 10, 64) + if err != nil { + return err + } + m.Uploaded = fromUnixMs(uploaded) + } + + m.MediaType = raw.MediaType + m.Tags = raw.Tags + + return nil +} + +// Tags is the result of List and Walk. +type Tags struct { + Children []string `json:"child"` + Manifests map[string]ManifestInfo `json:"manifest"` + Name string `json:"name"` + Tags []string `json:"tags"` +} + +// List calls /tags/list for the given repository. +func List(repo name.Repository, options ...ListerOption) (*Tags, error) { + l, err := newLister(repo, options...) + if err != nil { + return nil, err + } + + return l.list(repo) +} + +// WalkFunc is the type of the function called for each repository visited by +// Walk. This implements a similar API to filepath.Walk. +// +// The repo argument contains the argument to Walk as a prefix; that is, if Walk +// is called with "gcr.io/foo", which is a repository containing the repository +// "bar", the walk function will be called with argument "gcr.io/foo/bar". +// The tags and error arguments are the result of calling List on repo. +// +// TODO: Do we want a SkipDir error, as in filepath.WalkFunc? +type WalkFunc func(repo name.Repository, tags *Tags, err error) error + +func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...ListerOption) error { + if tags == nil { + // This shouldn't happen. + return fmt.Errorf("tags nil for %q", repo) + } + + if err := walkFn(repo, tags, nil); err != nil { + return err + } + + for _, path := range tags.Children { + child, err := name.NewRepository(fmt.Sprintf("%s/%s", repo, path), name.StrictValidation) + if err != nil { + // We don't expect this ever, so don't pass it through to walkFn. + return fmt.Errorf("unexpected path failure: %v", err) + } + + childTags, err := List(child, options...) + if err != nil { + if err := walkFn(child, nil, err); err != nil { + return err + } + } else { + if err := walk(child, childTags, walkFn, options...); err != nil { + return err + } + } + } + + // We made it! + return nil +} + +// Walk recursively descends repositories, calling walkFn. +func Walk(root name.Repository, walkFn WalkFunc, options ...ListerOption) error { + tags, err := List(root, options...) + if err != nil { + return walkFn(root, nil, err) + } + + return walk(root, tags, walkFn, options...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/google/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/google/options.go new file mode 100644 index 0000000000..6b6d590bc0 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/google/options.go @@ -0,0 +1,77 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "context" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" +) + +// WithTransport is a functional option for overriding the default transport +// on a remote image +func WithTransport(t http.RoundTripper) ListerOption { + return func(l *lister) error { + l.transport = t + return nil + } +} + +// WithAuth is a functional option for overriding the default authenticator +// on a remote image +func WithAuth(auth authn.Authenticator) ListerOption { + return func(l *lister) error { + l.auth = auth + return nil + } +} + +// WithAuthFromKeychain is a functional option for overriding the default +// authenticator on a remote image using an authn.Keychain +func WithAuthFromKeychain(keys authn.Keychain) ListerOption { + return func(l *lister) error { + auth, err := keys.Resolve(l.repo.Registry) + if err != nil { + return err + } + if auth == authn.Anonymous { + logs.Warn.Printf("No matching credentials were found for %q, falling back on anonymous", l.repo.Registry) + } + l.auth = auth + return nil + } +} + +// WithContext is a functional option for overriding the default +// context.Context for HTTP request to list remote images +func WithContext(ctx context.Context) ListerOption { + return func(l *lister) error { + l.ctx = ctx + return nil + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. This header will also include "go-containerregistry/${version}". +// +// If you want to completely overwrite the User-Agent header, use WithTransport. +func WithUserAgent(ua string) ListerOption { + return func(l *lister) error { + l.userAgent = ua + return nil + } +}