package registryclient import ( "context" "crypto/tls" "fmt" "io" "net" "net/http" "runtime" "time" ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/github" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/google" gcrremote "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/kyverno/kyverno/pkg/tracing" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "k8s.io/apimachinery/pkg/util/sets" corev1listers "k8s.io/client-go/listers/core/v1" "sigs.k8s.io/release-utils/version" ) var ( defaultKeychain = AnonymousKeychain defaultTransport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ // By default we wrap the transport in retries, so reduce the // default dial timeout to 5s to avoid 5x 30s of connection // timeouts when doing the "ping" on certain http registries. Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } userAgent = fmt.Sprintf("Kyverno/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH) ) // Client provides registry related objects. type Client interface { // Keychain provides the configured credentials Keychain() authn.Keychain // getTransport provides transport object. getTransport() http.RoundTripper // FetchImageDescriptor fetches Descriptor from registry with given imageRef // and provides access to metadata about remote artifact. FetchImageDescriptor(context.Context, string) (*gcrremote.Descriptor, error) // Options returns remote.Option configuration for the client. Options(context.Context) ([]gcrremote.Option, error) // NameOptions returns name.Option configuration for the client. NameOptions() []name.Option } type client struct { keychain authn.Keychain transport http.RoundTripper allowInsecureRegistry bool } type config struct { keychain []authn.Keychain transport *http.Transport tracing bool allowInsecureRegistry bool } // Option is an option to initialize registry client. type Option = func(*config) error // New creates a new Client with options func New(options ...Option) (Client, error) { cfg := &config{ transport: defaultTransport, } for _, opt := range options { if err := opt(cfg); err != nil { return nil, err } } c := &client{ keychain: defaultKeychain, transport: cfg.transport, } if len(cfg.keychain) > 0 { c.keychain = authn.NewMultiKeychain(cfg.keychain...) } if cfg.tracing { c.transport = tracing.Transport(cfg.transport, otelhttp.WithFilter(tracing.RequestFilterIsInSpan)) } if cfg.allowInsecureRegistry { c.allowInsecureRegistry = true } return c, nil } // New creates a new Client with options func NewOrDie(options ...Option) Client { c, err := New(options...) if err != nil { panic(err) } return c } // WithKeychainPullSecrets provides initialize registry client option that allows to use pull secrets. func WithKeychainPullSecrets(lister corev1listers.SecretNamespaceLister, imagePullSecrets ...string) Option { return func(c *config) error { kc, err := NewAutoRefreshSecretsKeychain(lister, imagePullSecrets...) if err != nil { return err } c.keychain = append(c.keychain, kc) return nil } } // WithCredentialProviders initialize registry client option by using registries credentials func WithCredentialProviders(credentialProviders ...string) Option { return func(c *config) error { var chains []authn.Keychain helpers := sets.New(credentialProviders...) if helpers.Has("default") { chains = append(chains, authn.DefaultKeychain) } if helpers.Has("google") { chains = append(chains, google.Keychain) } if helpers.Has("amazon") { chains = append(chains, authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))) } if helpers.Has("azure") { chains = append(chains, AzureKeychain) } if helpers.Has("github") { chains = append(chains, github.Keychain) } c.keychain = append(c.keychain, chains...) return nil } } // WithAllowInsecureRegistry initialize registry client option that allows to use insecure registries. func WithAllowInsecureRegistry() Option { return func(c *config) error { c.transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec c.allowInsecureRegistry = true return nil } } // WithLocalKeychain provides initialize keychain with the default local keychain. func WithLocalKeychain() Option { return func(c *config) error { c.keychain = append(c.keychain, authn.DefaultKeychain) return nil } } // WithTracing enables tracing in the http client. func WithTracing() Option { return func(c *config) error { c.tracing = true return nil } } // Options returns remote.Option config parameters for the client func (c *client) Options(ctx context.Context) ([]gcrremote.Option, error) { opts := []gcrremote.Option{ gcrremote.WithAuthFromKeychain(c.keychain), gcrremote.WithTransport(c.transport), gcrremote.WithContext(ctx), gcrremote.WithUserAgent(userAgent), } pusher, err := gcrremote.NewPusher(opts...) if err != nil { return nil, err } opts = append(opts, gcrremote.Reuse(pusher)) puller, err := gcrremote.NewPuller(opts...) if err != nil { return nil, err } opts = append(opts, gcrremote.Reuse(puller)) return opts, nil } // NameOptions returns name.Option config parameters for the client func (c *client) NameOptions() []name.Option { nameOpts := []name.Option{} if c.allowInsecureRegistry { nameOpts = append(nameOpts, name.Insecure) } return nameOpts } // FetchImageDescriptor fetches Descriptor from registry with given imageRef // and provides access to metadata about remote artifact. func (c *client) FetchImageDescriptor(ctx context.Context, imageRef string) (*gcrremote.Descriptor, error) { nameOpts := c.NameOptions() parsedRef, err := name.ParseReference(imageRef, nameOpts...) if err != nil { return nil, fmt.Errorf("failed to parse image reference: %s, error: %v", imageRef, err) } remoteOpts, err := c.Options(ctx) if err != nil { return nil, fmt.Errorf("failed to get gcr remote opts: %s, error: %v", imageRef, err) } desc, err := gcrremote.Get(parsedRef, remoteOpts...) if err != nil { return nil, fmt.Errorf("failed to fetch image reference: %s, error: %v", imageRef, err) } if _, ok := parsedRef.(name.Digest); ok && parsedRef.Identifier() != desc.Digest.String() { return nil, fmt.Errorf("digest mismatch, expected: %s, received: %s", parsedRef.Identifier(), desc.Digest.String()) } return desc, nil } func (c *client) Keychain() authn.Keychain { return c.keychain } func (c *client) getTransport() http.RoundTripper { return c.transport }