get.porter.sh/porter@v1.3.0/pkg/cnab/cnab-to-oci/registry.go (about)

     1  package cnabtooci
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"get.porter.sh/porter/pkg/cnab"
    12  	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
    13  	"get.porter.sh/porter/pkg/portercontext"
    14  	"get.porter.sh/porter/pkg/tracing"
    15  	"github.com/cnabio/cnab-go/driver/docker"
    16  	"github.com/cnabio/cnab-to-oci/relocation"
    17  	"github.com/cnabio/cnab-to-oci/remotes"
    18  	containerdRemotes "github.com/containerd/containerd/remotes"
    19  	"github.com/docker/cli/cli/command"
    20  	dockerconfig "github.com/docker/cli/cli/config"
    21  	"github.com/docker/docker/api/types/image"
    22  	registrytypes "github.com/docker/docker/api/types/registry"
    23  	"github.com/docker/docker/pkg/jsonmessage"
    24  	"github.com/google/go-containerregistry/pkg/crane"
    25  	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
    26  	"github.com/moby/term"
    27  	"github.com/opencontainers/go-digest"
    28  	"go.opentelemetry.io/otel/attribute"
    29  	"go.uber.org/zap/zapcore"
    30  )
    31  
    32  // ErrNoContentDigest represents an error due to an image not having a
    33  // corresponding content digest in a bundle definition
    34  type ErrNoContentDigest error
    35  
    36  // NewErrNoContentDigest returns an ErrNoContentDigest formatted with the
    37  // provided image name
    38  func NewErrNoContentDigest(image string) ErrNoContentDigest {
    39  	return fmt.Errorf("unable to verify that the pulled image %s is the bundle image referenced by the bundle because the bundle does not specify a content digest. This could allow for the bundle image to be replaced or tampered with", image)
    40  }
    41  
    42  var _ RegistryProvider = &Registry{}
    43  
    44  type Registry struct {
    45  	*portercontext.Context
    46  }
    47  
    48  func NewRegistry(c *portercontext.Context) *Registry {
    49  	return &Registry{
    50  		Context: c,
    51  	}
    52  }
    53  
    54  // PullBundle pulls a bundle from an OCI registry. Returns the bundle, and an optional image relocation mapping, if applicable.
    55  func (r *Registry) PullBundle(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (cnab.BundleReference, error) {
    56  	ctx, span := tracing.StartSpan(ctx,
    57  		attribute.String("reference", ref.String()),
    58  		attribute.Bool("insecure", opts.InsecureRegistry),
    59  	)
    60  	defer span.EndSpan()
    61  
    62  	var insecureRegistries []string
    63  	if opts.InsecureRegistry {
    64  		reg := ref.Registry()
    65  		insecureRegistries = append(insecureRegistries, reg)
    66  	}
    67  	resolver := r.createResolver(insecureRegistries)
    68  
    69  	if span.ShouldLog(zapcore.DebugLevel) {
    70  		msg := strings.Builder{}
    71  		msg.WriteString("Pulling bundle ")
    72  		msg.WriteString(ref.String())
    73  		if opts.InsecureRegistry {
    74  			msg.WriteString(" with --insecure-registry")
    75  		}
    76  		span.Debug(msg.String())
    77  	}
    78  
    79  	bun, reloMap, digest, err := remotes.Pull(ctx, ref.Named, resolver)
    80  	if err != nil {
    81  		if strings.Contains(err.Error(), "invalid media type") {
    82  			return cnab.BundleReference{}, span.Errorf("the provided reference must be a Porter bundle: %w", err)
    83  		}
    84  		return cnab.BundleReference{}, span.Errorf("unable to pull bundle: %w", err)
    85  	}
    86  
    87  	invocationImage := bun.InvocationImages[0]
    88  	if invocationImage.Digest == "" {
    89  		return cnab.BundleReference{}, span.Error(NewErrNoContentDigest(invocationImage.Image))
    90  	}
    91  
    92  	bundleRef := cnab.BundleReference{
    93  		Reference:     ref,
    94  		Digest:        digest,
    95  		Definition:    cnab.NewBundle(*bun),
    96  		RelocationMap: reloMap,
    97  	}
    98  
    99  	return bundleRef, nil
   100  }
   101  
   102  // PushBundle pushes a bundle to an OCI registry.
   103  func (r *Registry) PushBundle(ctx context.Context, bundleRef cnab.BundleReference, opts RegistryOptions) (cnab.BundleReference, error) {
   104  	ctx, log := tracing.StartSpan(ctx)
   105  	defer log.EndSpan()
   106  
   107  	var insecureRegistries []string
   108  	if opts.InsecureRegistry {
   109  		// Get all source registries
   110  		registries, err := bundleRef.Definition.GetReferencedRegistries()
   111  		if err != nil {
   112  			return cnab.BundleReference{}, err
   113  		}
   114  
   115  		// Include our destination registry
   116  		destReg := bundleRef.Reference.Registry()
   117  		found := false
   118  		for _, reg := range registries {
   119  			if destReg == reg {
   120  				found = true
   121  			}
   122  		}
   123  		if !found {
   124  			registries = append(registries, destReg)
   125  		}
   126  
   127  		// All registries used should be marked as allowing insecure connections
   128  		insecureRegistries = registries
   129  		log.SetAttributes(attribute.String("insecure-registries", strings.Join(registries, ",")))
   130  	}
   131  	resolver := r.createResolver(insecureRegistries)
   132  
   133  	if log.ShouldLog(zapcore.DebugLevel) {
   134  		msg := strings.Builder{}
   135  		msg.WriteString("Pushing bundle ")
   136  		msg.WriteString(bundleRef.String())
   137  		if opts.InsecureRegistry {
   138  			msg.WriteString(" with --insecure-registry")
   139  		}
   140  		log.Debug(msg.String())
   141  	}
   142  
   143  	// Initialize the relocation map if necessary
   144  	if bundleRef.RelocationMap == nil {
   145  		bundleRef.RelocationMap = make(relocation.ImageRelocationMap)
   146  	}
   147  	rm, err := remotes.FixupBundle(ctx, &bundleRef.Definition.Bundle, bundleRef.Reference.Named, resolver,
   148  		remotes.WithEventCallback(r.displayEvent),
   149  		remotes.WithAutoBundleUpdate(),
   150  		remotes.WithRelocationMap(bundleRef.RelocationMap))
   151  	if err != nil {
   152  		return cnab.BundleReference{}, log.Error(fmt.Errorf("error preparing the bundle with cnab-to-oci before pushing: %w", err))
   153  	}
   154  	bundleRef.RelocationMap = rm
   155  
   156  	d, err := remotes.Push(ctx, &bundleRef.Definition.Bundle, rm, bundleRef.Reference.Named, resolver, true)
   157  	if err != nil {
   158  		return cnab.BundleReference{}, log.Error(fmt.Errorf("error pushing the bundle to %s: %w", bundleRef.Reference, err))
   159  	}
   160  	bundleRef.Digest = d.Digest
   161  
   162  	stamp, err := configadapter.LoadStamp(bundleRef.Definition)
   163  	if err != nil {
   164  		return cnab.BundleReference{}, log.Errorf("error loading stamp from bundle: %w", err)
   165  	}
   166  	if stamp.PreserveTags {
   167  		err = preserveRelocatedImageTags(ctx, bundleRef, opts)
   168  		if err != nil {
   169  			return cnab.BundleReference{}, log.Error(fmt.Errorf("error preserving tags on relocated images: %w", err))
   170  		}
   171  	}
   172  
   173  	log.Infof("Bundle %s pushed successfully, with digest %q\n", bundleRef.Reference, d.Digest)
   174  	return bundleRef, nil
   175  }
   176  
   177  // PushImage pushes the image from the Docker image cache to the specified location
   178  // the expected format of the image is REGISTRY/NAME:TAG.
   179  // Returns the image digest from the registry.
   180  func (r *Registry) PushImage(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (digest.Digest, error) {
   181  	ctx, log := tracing.StartSpan(ctx)
   182  	defer log.EndSpan()
   183  
   184  	cli, err := docker.GetDockerClient()
   185  	if err != nil {
   186  		return "", log.Errorf("error creating a docker client: %w", err)
   187  	}
   188  
   189  	// Resolve the Repository name from fqn to RepositoryInfo
   190  	repoInfo, err := ref.ParseRepositoryInfo()
   191  	if err != nil {
   192  		return "", log.Errorf("error parsing the repository potion of the image reference %s: %w", ref, err)
   193  	}
   194  	authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index)
   195  	encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
   196  	if err != nil {
   197  		return "", log.Errorf("error encoding authentication information for the docker client: %w", err)
   198  	}
   199  	options := image.PushOptions{
   200  		RegistryAuth: encodedAuth,
   201  	}
   202  
   203  	log.Info("Pushing bundle image...")
   204  	pushResponse, err := cli.Client().ImagePush(ctx, ref.String(), options)
   205  	if err != nil {
   206  		return "", log.Errorf("docker push failed: %w", err)
   207  	}
   208  	defer pushResponse.Close()
   209  
   210  	termFd, _ := term.GetFdInfo(r.Out)
   211  	// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
   212  	// when Term is true.
   213  	isTerm := false
   214  	err = jsonmessage.DisplayJSONMessagesStream(pushResponse, r.Out, termFd, isTerm, nil)
   215  	if err != nil {
   216  		if strings.HasPrefix(err.Error(), "denied") {
   217  			return "", log.Errorf("docker push authentication failed: %w", err)
   218  		}
   219  		return "", log.Errorf("failed to stream docker push stdout: %w", err)
   220  	}
   221  	dist, err := cli.Client().DistributionInspect(ctx, ref.String(), encodedAuth)
   222  	if err != nil {
   223  		return "", log.Errorf("unable to inspect docker image: %w", err)
   224  	}
   225  	return dist.Descriptor.Digest, nil
   226  }
   227  
   228  // PullImage pulls an image from an OCI registry.
   229  func (r *Registry) PullImage(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) error {
   230  	ctx, log := tracing.StartSpan(ctx)
   231  	defer log.EndSpan()
   232  
   233  	cli, err := docker.GetDockerClient()
   234  	if err != nil {
   235  		return log.Error(err)
   236  	}
   237  
   238  	// Resolve the Repository name from fqn to RepositoryInfo
   239  	repoInfo, err := ref.ParseRepositoryInfo()
   240  	if err != nil {
   241  		return log.Error(err)
   242  	}
   243  	cli.ConfigFile()
   244  	authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index)
   245  	encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
   246  	if err != nil {
   247  		return log.Error(fmt.Errorf("failed to serialize docker auth config: %w", err))
   248  	}
   249  	options := image.PullOptions{
   250  		RegistryAuth: encodedAuth,
   251  	}
   252  
   253  	imgRef := ref.String()
   254  	rd, err := cli.Client().ImagePull(ctx, imgRef, options)
   255  	if err != nil {
   256  		return log.Error(fmt.Errorf("docker pull for image %s failed: %w", imgRef, err))
   257  	}
   258  	defer rd.Close()
   259  
   260  	// save the image to docker cache
   261  	_, err = io.ReadAll(rd)
   262  	if err != nil {
   263  		return fmt.Errorf("failed to save image %s into local cache: %w", imgRef, err)
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func (r *Registry) createResolver(insecureRegistries []string) containerdRemotes.Resolver {
   270  	return remotes.CreateResolver(dockerconfig.LoadDefaultConfigFile(r.Out), insecureRegistries...)
   271  }
   272  
   273  func (r *Registry) displayEvent(ev remotes.FixupEvent) {
   274  	switch ev.EventType {
   275  	case remotes.FixupEventTypeCopyImageStart:
   276  		fmt.Fprintf(r.Out, "Starting to copy image %s...\n", ev.SourceImage)
   277  	case remotes.FixupEventTypeCopyImageEnd:
   278  		if ev.Error != nil {
   279  			fmt.Fprintf(r.Out, "Failed to copy image %s: %s\n", ev.SourceImage, ev.Error)
   280  		} else {
   281  			fmt.Fprintf(r.Out, "Completed image %s copy\n", ev.SourceImage)
   282  		}
   283  	}
   284  }
   285  
   286  // GetCachedImage returns information about an image from local docker cache.
   287  func (r *Registry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error) {
   288  	image := ref.String()
   289  	ctx, log := tracing.StartSpan(ctx, attribute.String("reference", image))
   290  	defer log.EndSpan()
   291  
   292  	cli, err := docker.GetDockerClient()
   293  	if err != nil {
   294  		return ImageMetadata{}, log.Error(err)
   295  	}
   296  
   297  	result, err := cli.Client().ImageInspect(ctx, image)
   298  	if err != nil {
   299  		err = fmt.Errorf("failed to find image in docker cache: %w", ErrNotFound{Reference: ref})
   300  		// log as debug because this isn't a terminal error
   301  		log.Debugf(err.Error())
   302  		return ImageMetadata{}, err
   303  	}
   304  
   305  	summary, err := NewImageSummaryFromInspect(ref, result)
   306  	if err != nil {
   307  		return ImageMetadata{}, log.Error(fmt.Errorf("failed to extract image %s in docker cache: %w", image, err))
   308  	}
   309  
   310  	return summary, nil
   311  }
   312  
   313  func (r *Registry) ListTags(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) ([]string, error) {
   314  	// Get the fully-qualified repository name, including docker.io (required by crane)
   315  	repository := ref.Named.Name()
   316  
   317  	_, span := tracing.StartSpan(ctx, attribute.String("repository", repository))
   318  	defer span.EndSpan()
   319  
   320  	tags, err := crane.ListTags(repository, opts.toCraneOptions()...)
   321  	if err != nil {
   322  		if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil {
   323  			return nil, span.Error(notFoundErr)
   324  		}
   325  		return nil, span.Errorf("error listing tags for %s: %w", ref.String(), err)
   326  	}
   327  
   328  	return tags, nil
   329  }
   330  
   331  // GetBundleMetadata returns information about a bundle in a registry
   332  // Use ErrNotFound to detect if the error is because the bundle is not in the registry.
   333  func (r *Registry) GetBundleMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (BundleMetadata, error) {
   334  	_, span := tracing.StartSpan(ctx, attribute.String("reference", ref.String()))
   335  	defer span.EndSpan()
   336  
   337  	bundleDigest, err := crane.Digest(ref.String(), opts.toCraneOptions()...)
   338  	if err != nil {
   339  		if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil {
   340  			return BundleMetadata{}, span.Error(notFoundErr)
   341  		}
   342  		return BundleMetadata{}, span.Errorf("error retrieving bundle metadata for %s: %w", ref.String(), err)
   343  	}
   344  
   345  	return BundleMetadata{
   346  		BundleReference: cnab.BundleReference{
   347  			Reference: ref,
   348  			Digest:    digest.Digest(bundleDigest),
   349  		},
   350  	}, nil
   351  }
   352  
   353  // GetImageMetadata returns information about an image in a registry
   354  // Use ErrNotFound to detect if the error is because the image is not in the registry.
   355  func (r *Registry) GetImageMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error) {
   356  	ctx, span := tracing.StartSpan(ctx, attribute.String("reference", ref.String()))
   357  	defer span.EndSpan()
   358  
   359  	// Check if we already have the image in the Docker cache
   360  	cachedResult, err := r.GetCachedImage(ctx, ref)
   361  	if err != nil {
   362  		if !errors.Is(err, ErrNotFound{}) {
   363  			return ImageMetadata{}, err
   364  		}
   365  	}
   366  
   367  	// Check if we have the repository digest cached for the referenced image
   368  	if cachedDigest, err := cachedResult.GetRepositoryDigest(); err == nil {
   369  		span.SetAttributes(attribute.String("cached-digest", cachedDigest.String()))
   370  		return cachedResult, nil
   371  	}
   372  
   373  	// Do a HEAD against the registry to retrieve image metadata without pulling the entire image contents
   374  	desc, err := crane.Head(ref.String(), opts.toCraneOptions()...)
   375  	if err != nil {
   376  		if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil {
   377  			return ImageMetadata{}, span.Error(notFoundErr)
   378  		}
   379  		return ImageMetadata{}, span.Errorf("error fetching image metadata for %s: %w", ref, err)
   380  	}
   381  
   382  	repoDigest := digest.NewDigestFromHex(desc.Digest.Algorithm, desc.Digest.Hex)
   383  	span.SetAttributes(attribute.String("fetched-digest", repoDigest.String()))
   384  
   385  	return NewImageSummaryFromDigest(ref, repoDigest)
   386  }
   387  
   388  // asNotFoundError checks if the error is an HTTP 404 not found error, and if so returns a corresponding ErrNotFound instance.
   389  func asNotFoundError(err error, ref cnab.OCIReference) error {
   390  	var httpError *transport.Error
   391  	if errors.As(err, &httpError) {
   392  		if httpError.StatusCode == http.StatusNotFound {
   393  			return ErrNotFound{Reference: ref}
   394  		}
   395  	}
   396  
   397  	return nil
   398  }
   399  
   400  // ImageMetadata contains information about an OCI image.
   401  type ImageMetadata struct {
   402  	Reference   cnab.OCIReference
   403  	RepoDigests []string
   404  }
   405  
   406  func NewImageSummaryFromInspect(ref cnab.OCIReference, sum image.InspectResponse) (ImageMetadata, error) {
   407  	img := ImageMetadata{
   408  		Reference:   ref,
   409  		RepoDigests: sum.RepoDigests,
   410  	}
   411  	if img.IsZero() {
   412  		return ImageMetadata{}, fmt.Errorf("invalid image summary for image reference %s", ref)
   413  	}
   414  
   415  	return img, nil
   416  }
   417  
   418  func NewImageSummaryFromDigest(ref cnab.OCIReference, repoDigest digest.Digest) (ImageMetadata, error) {
   419  	digestedRef, err := ref.WithDigest(repoDigest)
   420  	if err != nil {
   421  		return ImageMetadata{}, fmt.Errorf("error building an OCI reference from image %s and digest %s", ref.Repository(), ref.Digest())
   422  	}
   423  
   424  	return ImageMetadata{
   425  		Reference:   ref,
   426  		RepoDigests: []string{digestedRef.String()},
   427  	}, nil
   428  }
   429  
   430  func (i ImageMetadata) String() string {
   431  	return i.Reference.String()
   432  }
   433  
   434  func (i ImageMetadata) IsZero() bool {
   435  	return i.String() == ""
   436  }
   437  
   438  // GetRepositoryDigest finds the repository digest associated with the original
   439  // image reference used to create this ImageMetadata.
   440  func (i ImageMetadata) GetRepositoryDigest() (digest.Digest, error) {
   441  	if len(i.RepoDigests) == 0 {
   442  		return "", fmt.Errorf("failed to get digest for image: %s", i)
   443  	}
   444  	var imgDigest digest.Digest
   445  	for _, rd := range i.RepoDigests {
   446  		imgRef, err := cnab.ParseOCIReference(rd)
   447  		if err != nil {
   448  			return "", err
   449  		}
   450  		if imgRef.Repository() != i.Reference.Repository() {
   451  			continue
   452  		}
   453  
   454  		if !imgRef.HasDigest() {
   455  			return "", fmt.Errorf("image summary does not contain digest for image: %s", imgRef.String())
   456  		}
   457  
   458  		imgDigest = imgRef.Digest()
   459  		break
   460  	}
   461  
   462  	if imgDigest == "" {
   463  		return "", fmt.Errorf("cannot find image digest for desired repo %s", i)
   464  	}
   465  
   466  	if err := imgDigest.Validate(); err != nil {
   467  		return "", err
   468  	}
   469  
   470  	return imgDigest, nil
   471  }
   472  
   473  // GetInsecureRegistryTransport returns a copy of the default http transport
   474  // with InsecureSkipVerify set so that we can use it with insecure registries.
   475  func GetInsecureRegistryTransport() *http.Transport {
   476  	skipTLS := http.DefaultTransport.(*http.Transport)
   477  	skipTLS = skipTLS.Clone()
   478  	skipTLS.TLSClientConfig.InsecureSkipVerify = true
   479  	return skipTLS
   480  }
   481  
   482  func preserveRelocatedImageTags(ctx context.Context, bundleRef cnab.BundleReference, opts RegistryOptions) error {
   483  	_, log := tracing.StartSpan(ctx)
   484  	defer log.EndSpan()
   485  
   486  	if len(bundleRef.Definition.Images) <= 0 {
   487  		log.Debugf("No images to preserve tags on")
   488  		return nil
   489  	}
   490  
   491  	log.Infof("Tagging relocated images...")
   492  	for _, image := range bundleRef.Definition.Images {
   493  		imageRef, err := cnab.ParseOCIReference(image.Image)
   494  		if err != nil {
   495  			return log.Errorf("error parsing image reference %s: %w", image.Image, err)
   496  		}
   497  
   498  		if !imageRef.HasTag() {
   499  			log.Debugf("Image %s has no tag, skipping", imageRef)
   500  			continue
   501  		}
   502  
   503  		if relocImage, ok := bundleRef.RelocationMap[image.Image]; ok {
   504  			relocRef, err := cnab.ParseOCIReference(relocImage)
   505  			if err != nil {
   506  				return log.Errorf("error parsing image reference %s: %w", relocImage, err)
   507  			}
   508  
   509  			dstRef := fmt.Sprintf("%s/%s:%s", relocRef.Registry(), imageRef.Repository(), imageRef.Tag())
   510  			log.Debugf("Copying image %s to %s", relocRef, dstRef)
   511  			err = crane.Copy(relocRef.String(), dstRef, opts.toCraneOptions()...)
   512  			if err != nil {
   513  				return log.Errorf("error copying image %s to %s: %w", relocRef, dstRef, err)
   514  			}
   515  		} else {
   516  			log.Debugf("No relocation for image %s", imageRef)
   517  		}
   518  	}
   519  
   520  	return nil
   521  }