function-runtime-oci/internal/oci/pull.go

251 lines
7.4 KiB
Go

/*
Copyright 2022 The Crossplane Authors.
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 oci
import (
"context"
"crypto/tls"
"crypto/x509"
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
ociv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/crossplane/crossplane-runtime/pkg/errors"
)
// Error strings.
const (
errPullNever = "refusing to pull from remote with image pull policy " + string(ImagePullPolicyNever)
errNewDigestStore = "cannot create new image digest store"
errPullImage = "cannot pull image from remote"
errStoreImage = "cannot cache image"
errImageDigest = "cannot get image digest"
errStoreDigest = "cannot cache image digest"
errLoadImage = "cannot load image from cache"
errLoadHash = "cannot load image digest"
)
// An ImagePullPolicy dictates when an image may be pulled from a remote.
type ImagePullPolicy string
// Image pull policies
const (
// ImagePullPolicyIfNotPresent only pulls from a remote if the image is not
// in the local cache. It is equivalent to ImagePullPolicyNever with a
// fall-back to ImagePullPolicyAlways.
ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent"
// ImagePullPolicyAlways always pulls at least the image manifest from the
// remote. Layers are pulled if they are not in cache.
ImagePullPolicyAlways ImagePullPolicy = "Always"
// ImagePullPolicyNever never pulls anything from the remote. It resolves
// OCI references to digests (i.e. SHAs) using a local cache of known
// mappings.
ImagePullPolicyNever ImagePullPolicy = "Never"
)
// ImagePullAuth configures authentication to a remote registry.
type ImagePullAuth struct {
Username string
Password string
Auth string
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string
// RegistryToken is a bearer token to be sent to a registry.
RegistryToken string
}
// Authorization builds a go-containerregistry compatible AuthConfig.
func (a ImagePullAuth) Authorization() (*authn.AuthConfig, error) {
return &authn.AuthConfig{
Username: a.Username,
Password: a.Password,
Auth: a.Auth,
IdentityToken: a.IdentityToken,
RegistryToken: a.RegistryToken,
}, nil
}
// ImageClientOptions configure an ImageClient.
type ImageClientOptions struct {
pull ImagePullPolicy
auth *ImagePullAuth
transport *http.Transport
}
func parse(o ...ImageClientOption) ImageClientOptions {
opt := &ImageClientOptions{
pull: ImagePullPolicyIfNotPresent, // The default.
}
for _, fn := range o {
fn(opt)
}
return *opt
}
// An ImageClientOption configures an ImageClient.
type ImageClientOption func(c *ImageClientOptions)
// WithPullPolicy specifies whether a client may pull from a remote.
func WithPullPolicy(p ImagePullPolicy) ImageClientOption {
return func(c *ImageClientOptions) {
c.pull = p
}
}
// WithPullAuth specifies how a client should authenticate to a remote.
func WithPullAuth(a *ImagePullAuth) ImageClientOption {
return func(c *ImageClientOptions) {
c.auth = a
}
}
// WithCustomCA adds given root certificates to tls client configuration
func WithCustomCA(rootCAs *x509.CertPool) ImageClientOption {
return func(c *ImageClientOptions) {
c.transport = remote.DefaultTransport.(*http.Transport).Clone()
c.transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs, MinVersion: tls.VersionTLS12}
}
}
// An ImageClient is an OCI registry client.
type ImageClient interface {
// Image pulls an OCI image.
Image(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error)
}
// An ImageCache caches OCI images.
type ImageCache interface {
Image(h ociv1.Hash) (ociv1.Image, error)
WriteImage(img ociv1.Image) error
}
// A HashCache maps OCI references to hashes.
type HashCache interface {
Hash(r name.Reference) (ociv1.Hash, error)
WriteHash(r name.Reference, h ociv1.Hash) error
}
// A RemoteClient fetches OCI image manifests.
type RemoteClient struct{}
// Image fetches an image manifest. The returned image lazily pulls its layers.
func (i *RemoteClient) Image(ctx context.Context, ref name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
opts := parse(o...)
iOpts := []remote.Option{remote.WithContext(ctx)}
if opts.auth != nil {
iOpts = append(iOpts, remote.WithAuth(opts.auth))
}
if opts.transport != nil {
iOpts = append(iOpts, remote.WithTransport(opts.transport))
}
if opts.pull == ImagePullPolicyNever {
return nil, errors.New(errPullNever)
}
return remote.Image(ref, iOpts...)
}
// A CachingPuller pulls OCI images. Images are pulled either from a local cache
// or a remote depending on whether they are available locally and a supplied
// ImagePullPolicy.
type CachingPuller struct {
remote ImageClient
local ImageCache
mapping HashCache
}
// NewCachingPuller returns an OCI image puller with a local cache.
func NewCachingPuller(h HashCache, i ImageCache, r ImageClient) *CachingPuller {
return &CachingPuller{remote: r, local: i, mapping: h}
}
// Image pulls the supplied image and all of its layers. The supplied config
// determines where the image may be pulled from - i.e. the local store or a
// remote. Images that are pulled from a remote are cached in the local store.
func (f *CachingPuller) Image(ctx context.Context, r name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
opts := parse(o...)
switch opts.pull {
case ImagePullPolicyNever:
return f.never(r)
case ImagePullPolicyAlways:
return f.always(ctx, r, o...)
case ImagePullPolicyIfNotPresent:
fallthrough
default:
img, err := f.never(r)
if err == nil {
return img, nil
}
return f.always(ctx, r, o...)
}
}
func (f *CachingPuller) never(r name.Reference) (ociv1.Image, error) {
var h ociv1.Hash
var err error
// Avoid a cache lookup if the digest was specified explicitly.
switch d := r.(type) {
case name.Digest:
h, err = ociv1.NewHash(d.DigestStr())
default:
h, err = f.mapping.Hash(r)
}
if err != nil {
return nil, errors.Wrap(err, errLoadHash)
}
i, err := f.local.Image(h)
return i, errors.Wrap(err, errLoadImage)
}
func (f *CachingPuller) always(ctx context.Context, r name.Reference, o ...ImageClientOption) (ociv1.Image, error) {
// This will only pull the image's manifest and config, not layers.
img, err := f.remote.Image(ctx, r, o...)
if err != nil {
return nil, errors.Wrap(err, errPullImage)
}
// This will fetch any layers that aren't already in the store.
if err := f.local.WriteImage(img); err != nil {
return nil, errors.Wrap(err, errStoreImage)
}
d, err := img.Digest()
if err != nil {
return nil, errors.Wrap(err, errImageDigest)
}
// Store a mapping from this reference to its digest.
if err := f.mapping.WriteHash(r, d); err != nil {
return nil, errors.Wrap(err, errStoreDigest)
}
// Return the stored image to ensure future reads are from disk, not
// from remote.
img, err = f.local.Image(d)
return img, errors.Wrap(err, errLoadImage)
}