Fix implicit GCR auth (#1856)

* Fix implicit GCR auth

* boilerplate
This commit is contained in:
Jason Hall 2021-12-28 20:34:32 -05:00 committed by GitHub
parent 129df249c9
commit 633f555c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 693 additions and 60 deletions

View File

@ -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"))
}

View File

@ -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
}

View File

@ -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
}

37
pkg/creds/gcr_keychain.go Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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
}
}