get.porter.sh/porter@v1.3.0/pkg/porter/publish.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"get.porter.sh/porter/pkg/build"
    12  	"get.porter.sh/porter/pkg/cnab"
    13  	cnabtooci "get.porter.sh/porter/pkg/cnab/cnab-to-oci"
    14  	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
    15  	"get.porter.sh/porter/pkg/config"
    16  	"get.porter.sh/porter/pkg/manifest"
    17  	"get.porter.sh/porter/pkg/tracing"
    18  	"github.com/cnabio/cnab-go/bundle/loader"
    19  	"github.com/cnabio/cnab-go/packager"
    20  	"github.com/cnabio/cnab-to-oci/relocation"
    21  	"github.com/cnabio/image-relocation/pkg/image"
    22  	"github.com/cnabio/image-relocation/pkg/registry"
    23  	"github.com/cnabio/image-relocation/pkg/registry/ggcr"
    24  	"github.com/opencontainers/go-digest"
    25  )
    26  
    27  // PublishOptions are options that may be specified when publishing a bundle.
    28  // Porter handles defaulting any missing values.
    29  type PublishOptions struct {
    30  	BundlePullOptions
    31  	BundleDefinitionOptions
    32  	Tag         string
    33  	Registry    string
    34  	ArchiveFile string
    35  	SignBundle  bool
    36  }
    37  
    38  // Validate performs validation on the publish options
    39  func (o *PublishOptions) Validate(cfg *config.Config) error {
    40  	if o.ArchiveFile != "" {
    41  		// Verify the archive file can be accessed
    42  		if _, err := cfg.FileSystem.Stat(o.ArchiveFile); err != nil {
    43  			return fmt.Errorf("unable to access --archive %s: %w", o.ArchiveFile, err)
    44  		}
    45  
    46  		if o.Reference == "" {
    47  			return errors.New("must provide a value for --reference of the form REGISTRY/bundle:tag")
    48  		}
    49  	} else {
    50  		// Proceed with publishing from the resolved build context directory
    51  		err := o.BundleDefinitionOptions.Validate(cfg.Context)
    52  		if err != nil {
    53  			return err
    54  		}
    55  
    56  		if o.File == "" {
    57  			return fmt.Errorf("could not find porter.yaml in the current directory %s, make sure you are in the right directory or specify the porter manifest with --file", o.Dir)
    58  		}
    59  	}
    60  
    61  	if o.Reference != "" {
    62  		return o.BundlePullOptions.Validate()
    63  	}
    64  
    65  	if o.Tag != "" {
    66  		return o.validateTag()
    67  	}
    68  
    69  	// Apply the global config for force overwrite
    70  	if !o.Force && cfg.Data.ForceOverwrite {
    71  		o.Force = true
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  // validateTag checks to make sure the supplied tag is of the expected form.
    78  // A previous iteration of this flag was used to designate an entire bundle
    79  // reference.  If we detect this attempted use, we return an error and
    80  // explanation
    81  func (o *PublishOptions) validateTag() error {
    82  	if strings.Contains(o.Tag, ":") || strings.Contains(o.Tag, "@") {
    83  		return errors.New("the --tag flag has been updated to designate just the Docker tag portion of the bundle reference; use --reference for the full bundle reference instead")
    84  	}
    85  	return nil
    86  }
    87  
    88  // Publish is a composite function that publishes an bundle image, rewrites the porter manifest
    89  // and then regenerates the bundle.json. Finally, it publishes the manifest to an OCI registry.
    90  func (p *Porter) Publish(ctx context.Context, opts PublishOptions) error {
    91  	ctx, log := tracing.StartSpan(ctx)
    92  	defer log.EndSpan()
    93  
    94  	if opts.ArchiveFile == "" {
    95  		return p.publishFromFile(ctx, opts)
    96  	}
    97  	return p.publishFromArchive(ctx, opts)
    98  }
    99  
   100  func (p *Porter) publishFromFile(ctx context.Context, opts PublishOptions) error {
   101  	ctx, log := tracing.StartSpan(ctx)
   102  	defer log.EndSpan()
   103  
   104  	buildOpts := BuildOptions{
   105  		BundleDefinitionOptions: opts.BundleDefinitionOptions,
   106  		InsecureRegistry:        opts.InsecureRegistry,
   107  	}
   108  	bundleRef, err := p.ensureLocalBundleIsUpToDate(ctx, buildOpts)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// If the manifest file is the default/user-supplied manifest,
   114  	// hot-swap in Porter's canonical translation (if exists) from
   115  	// the .cnab/app directory, as there may be dynamic overrides for
   116  	// the name and version fields to inform bundle image naming.
   117  	canonicalManifest := filepath.Join(opts.Dir, build.LOCAL_MANIFEST)
   118  	canonicalExists, err := p.FileSystem.Exists(canonicalManifest)
   119  	if err != nil {
   120  		return log.Errorf("error reading manifest %s: %w", canonicalManifest)
   121  	}
   122  
   123  	var m *manifest.Manifest
   124  	if canonicalExists {
   125  		m, err = manifest.LoadManifestFrom(ctx, p.Config, canonicalManifest)
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  		// We still want the user-provided manifest path to be tracked,
   131  		// not Porter's canonical manifest path, for digest matching/auto-rebuilds
   132  		m.ManifestPath = opts.File
   133  	} else {
   134  		m, err = manifest.LoadManifestFrom(ctx, p.Config, opts.File)
   135  		if err != nil {
   136  			return err
   137  		}
   138  	}
   139  
   140  	// Capture original bundle image name as it may be updated below
   141  	origInvImg := m.Image
   142  
   143  	// Check for tag and registry overrides optionally supplied on publish
   144  	if opts.Tag != "" {
   145  		m.DockerTag = opts.Tag
   146  	}
   147  	if opts.Registry != "" {
   148  		m.Registry = opts.Registry
   149  	}
   150  
   151  	// If either are non-empty, null out the reference on the manifest, as
   152  	// it needs to be rebuilt with new values
   153  	if opts.Tag != "" || opts.Registry != "" {
   154  		m.Reference = ""
   155  	}
   156  
   157  	// Update bundle image and reference with opts.Reference, which may be
   158  	// empty, which is fine - we still may need to pick up tag and/or registry
   159  	// overrides
   160  	if err := m.SetBundleImageAndReference(opts.Reference); err != nil {
   161  		return log.Errorf("unable to set bundle image name and reference: %w", err)
   162  	}
   163  
   164  	if origInvImg != m.Image {
   165  		// Tag it so that it will be known/found by Docker for publishing
   166  		builder := p.GetBuilder(ctx)
   167  		if err := builder.TagBundleImage(ctx, origInvImg, m.Image); err != nil {
   168  			return err
   169  		}
   170  	}
   171  
   172  	if m.Reference == "" {
   173  		return log.Errorf("porter.yaml is missing registry or reference values needed for publishing")
   174  	}
   175  
   176  	// var bundleRef cnab.BundleReference
   177  	bundleRef.Reference, err = cnab.ParseOCIReference(m.Reference)
   178  	if err != nil {
   179  		return log.Errorf("invalid reference %s: %w", m.Reference, err)
   180  	}
   181  
   182  	imgRef, err := cnab.ParseOCIReference(m.Image)
   183  	if err != nil {
   184  		return log.Errorf("error parsing %s as an OCI reference: %w", m.Image, err)
   185  	}
   186  
   187  	regOpts := cnabtooci.RegistryOptions{
   188  		InsecureRegistry: opts.InsecureRegistry,
   189  	}
   190  
   191  	// Before we attempt to push, check if any of the bundle exists already.
   192  	// If force was not specified, we shouldn't push any of the bundle since
   193  	// the bundle and images must be pushed as a unit.
   194  	if !opts.Force {
   195  		_, err := p.Registry.GetBundleMetadata(ctx, bundleRef.Reference, regOpts)
   196  		if err != nil {
   197  			if !errors.Is(err, cnabtooci.ErrNotFound{}) {
   198  				return log.Errorf("Publish stopped because detection of %s in the destination registry failed. To overwrite it, repeat the command with --force specified: %w", bundleRef, err)
   199  			}
   200  		} else {
   201  			return log.Errorf("Publish stopped because %s already exists in the destination registry. To overwrite it, repeat the command with --force specified.", bundleRef)
   202  		}
   203  	}
   204  
   205  	bundleRef.Digest, err = p.Registry.PushImage(ctx, imgRef, regOpts)
   206  	if err != nil {
   207  		return log.Errorf("unable to push bundle image %q: %w", m.Image, err)
   208  	}
   209  
   210  	stamp, err := configadapter.LoadStamp(bundleRef.Definition)
   211  	if err != nil {
   212  		return log.Errorf("failed to load stamp from bundle definition: %w", err)
   213  	}
   214  	bundleRef.Definition, err = p.rewriteBundleWithBundleImageDigest(ctx, m, bundleRef.Digest, stamp.PreserveTags)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	bundleRef, err = p.Registry.PushBundle(ctx, bundleRef, regOpts)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	if opts.SignBundle {
   225  		log.Debugf("signing bundle %s", bundleRef.String())
   226  		inImage, err := cnab.CalculateTemporaryImageTag(bundleRef.Reference)
   227  		if err != nil {
   228  			return log.Errorf("error calculation temporary image tag: %w", err)
   229  		}
   230  		log.Debugf("Signing bundle image %s.", inImage.String())
   231  		err = p.signImage(ctx, inImage)
   232  		if err != nil {
   233  			return log.Errorf("error signing bundle image: %w", err)
   234  		}
   235  		log.Debugf("Signing bundle artifact %s.", bundleRef.Reference.String())
   236  		err = p.signImage(ctx, bundleRef.Reference)
   237  		if err != nil {
   238  			return log.Errorf("error signing bundle artifact: %w", err)
   239  		}
   240  	}
   241  
   242  	// Perhaps we have a cached version of a bundle with the same reference, previously pulled
   243  	// If so, replace it, as it is most likely out-of-date per this publish
   244  	err = p.refreshCachedBundle(bundleRef)
   245  	return log.Error(err)
   246  }
   247  
   248  // publishFromArchive (re-)publishes a bundle, provided by the archive file, using the provided tag.
   249  //
   250  // After the bundle is extracted from the archive, we iterate through all of the images (bundle
   251  // and application) listed in the bundle, grab their digests by parsing the extracted
   252  // OCI Layout, rename each based on the registry/org values derived from the provided tag
   253  // and then push each updated image with the original digests
   254  //
   255  // Finally, we update the relocation map in the original bundle, based
   256  // on the newly copied images, and then push the bundle using the provided tag.
   257  // (Currently we use the docker/cnab-to-oci library for this logic.)
   258  func (p *Porter) publishFromArchive(ctx context.Context, opts PublishOptions) error {
   259  	ctx, log := tracing.StartSpan(ctx)
   260  	defer log.EndSpan()
   261  
   262  	regOpts := cnabtooci.RegistryOptions{InsecureRegistry: opts.InsecureRegistry}
   263  
   264  	// Before we attempt to push, check if any of the bundle exists already.
   265  	// If force was not specified, we shouldn't push any of the bundle since
   266  	// the bundle and images must be pushed as a unit.
   267  	ref := opts.GetReference()
   268  	if !opts.Force {
   269  		_, err := p.Registry.GetBundleMetadata(ctx, ref, regOpts)
   270  		if err != nil {
   271  			if !errors.Is(err, cnabtooci.ErrNotFound{}) {
   272  				return log.Errorf("Publish stopped because detection of %s in the destination registry failed. To overwrite it, repeat the command with --force specified: %w", ref, err)
   273  			}
   274  		} else {
   275  			return log.Errorf("Publish stopped because %s already exists in the destination registry. To overwrite it, repeat the command with --force specified.", ref)
   276  		}
   277  	}
   278  
   279  	source := p.FileSystem.Abs(opts.ArchiveFile)
   280  	tmpDir, err := p.FileSystem.TempDir("", "porter")
   281  	if err != nil {
   282  		return log.Errorf("error creating temp directory for archive extraction: %w", err)
   283  	}
   284  	defer func() {
   285  		err = errors.Join(err, p.FileSystem.RemoveAll(tmpDir))
   286  	}()
   287  
   288  	bundleRef, err := p.extractBundle(ctx, tmpDir, source)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	bundleRef.Reference = ref
   294  
   295  	log.Infof("Beginning bundle publish to %s. This may take some time.", opts.Reference)
   296  
   297  	// Use the ggcr client to read the extracted OCI Layout
   298  	extractedDir := filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz"))
   299  	var clientOpts []ggcr.Option
   300  	if opts.InsecureRegistry {
   301  		skipTLS := cnabtooci.GetInsecureRegistryTransport()
   302  		clientOpts = append(clientOpts, ggcr.WithTransport(skipTLS))
   303  	}
   304  	client := ggcr.NewRegistryClient(clientOpts...)
   305  	layout, err := client.ReadLayout(filepath.Join(extractedDir, "artifacts/layout"))
   306  	if err != nil {
   307  		return log.Errorf("failed to parse OCI Layout from archive %s: %w", opts.ArchiveFile, err)
   308  	}
   309  
   310  	// Push updated images (renamed based on provided bundle tag) with same digests
   311  	// then update the bundle with new values (image name, digest)
   312  	for _, invImg := range bundleRef.Definition.InvocationImages {
   313  		relocMap, err := p.relocateImage(bundleRef.RelocationMap, layout, invImg.Image, opts.Reference)
   314  		if err != nil {
   315  			return log.Error(err)
   316  		}
   317  
   318  		bundleRef.RelocationMap = relocMap
   319  
   320  		if opts.SignBundle {
   321  			relocInvImage := relocMap[invImg.Image]
   322  			log.Debugf("Signing bundle image %s...", relocInvImage)
   323  			invImageRef, err := cnab.ParseOCIReference(relocInvImage)
   324  			if err != nil {
   325  				return log.Errorf("failed to parse OCI reference %s: %w", relocInvImage, err)
   326  			}
   327  			err = p.signImage(ctx, invImageRef)
   328  			if err != nil {
   329  				return log.Errorf("failed to sign image %s: %w", invImageRef.String(), err)
   330  			}
   331  		}
   332  	}
   333  	for _, img := range bundleRef.Definition.Images {
   334  		relocMap, err := p.relocateImage(bundleRef.RelocationMap, layout, img.Image, opts.Reference)
   335  		if err != nil {
   336  			return log.Error(err)
   337  		}
   338  
   339  		bundleRef.RelocationMap = relocMap
   340  	}
   341  
   342  	bundleRef, err = p.Registry.PushBundle(ctx, bundleRef, regOpts)
   343  	if err != nil {
   344  		return err
   345  	}
   346  
   347  	if opts.SignBundle {
   348  		log.Debugf("Signing bundle %s...", bundleRef.String())
   349  		err = p.signImage(ctx, bundleRef.Reference)
   350  		if err != nil {
   351  			return log.Errorf("failed to sign bundle %s: %w", bundleRef.String(), err)
   352  		}
   353  	}
   354  
   355  	// Perhaps we have a cached version of a bundle with the same tag, previously pulled
   356  	// If so, replace it, as it is most likely out-of-date per this publish
   357  	err = p.refreshCachedBundle(bundleRef)
   358  	return log.Error(err)
   359  }
   360  
   361  // extractBundle extracts a bundle using the provided opts and returns the extracted bundle
   362  func (p *Porter) extractBundle(ctx context.Context, tmpDir, source string) (cnab.BundleReference, error) {
   363  	_, span := tracing.StartSpan(ctx)
   364  	defer span.EndSpan()
   365  
   366  	span.Debugf("Extracting bundle from archive %s...", source)
   367  	l := loader.NewLoader()
   368  	imp := packager.NewImporter(source, tmpDir, l)
   369  	err := imp.Import()
   370  	if err != nil {
   371  		return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to extract bundle from archive %s: %w", source, err))
   372  	}
   373  
   374  	bun, err := l.Load(filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz"), "bundle.json"))
   375  	if err != nil {
   376  		return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to load bundle from archive %s: %w", source, err))
   377  	}
   378  	data, err := p.FileSystem.ReadFile(filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz"), "relocation-mapping.json"))
   379  	if err != nil {
   380  		return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to load relocation-mapping.json from archive %s: %w", source, err))
   381  	}
   382  	var reloMap relocation.ImageRelocationMap
   383  	err = json.Unmarshal(data, &reloMap)
   384  	if err != nil {
   385  		return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to parse relocation-mapping.json from archive %s: %w", source, err))
   386  	}
   387  
   388  	return cnab.BundleReference{Definition: cnab.ExtendedBundle{Bundle: *bun}, RelocationMap: reloMap}, nil
   389  }
   390  
   391  // pushUpdatedImage uses the provided layout to find the provided origImg,
   392  // gathers the pre-existing digest and then pushes this digest using the newImgName
   393  func pushUpdatedImage(layout registry.Layout, origImg string, newImgName image.Name) (image.Digest, error) {
   394  	origImgName, err := image.NewName(origImg)
   395  	if err != nil {
   396  		return image.EmptyDigest, fmt.Errorf("unable to parse image %q into domain/path components: %w", origImg, err)
   397  	}
   398  
   399  	digest, err := layout.Find(origImgName)
   400  	if err != nil {
   401  		return image.EmptyDigest, fmt.Errorf("unable to find image %s in archived OCI Layout: %w", origImgName.String(), err)
   402  	}
   403  
   404  	err = layout.Push(digest, newImgName)
   405  	if err != nil {
   406  		return image.EmptyDigest, fmt.Errorf("unable to push image %s: %w", newImgName.String(), err)
   407  	}
   408  
   409  	return digest, nil
   410  }
   411  
   412  // getNewImageNameFromBundleReference derives a new image.Name object from the provided original
   413  // image (string) using the provided bundleTag to clean registry/org/etc.
   414  func getNewImageNameFromBundleReference(origImg, bundleTag string) (image.Name, error) {
   415  	origImgRef, err := cnab.ParseOCIReference(origImg)
   416  	if err != nil {
   417  		return image.EmptyName, err
   418  	}
   419  
   420  	bundleRef, err := cnab.ParseOCIReference(bundleTag)
   421  	if err != nil {
   422  		return image.EmptyName, err
   423  	}
   424  
   425  	// Calculate a unique tag based on the original referenced image. It is safe to
   426  	// use only the original image, and not a combination of both the destination and
   427  	// the source to create a unique value, because we rewrite the referenced image
   428  	// to always use a repository digest. The only time two images will have the same
   429  	// source value is when they are the same image and have the same content. In
   430  	// which case it is okay if two bundles both reference the same image and reuse
   431  	// the same temporary tag because the content is the same.
   432  	tmpImage, err := cnab.CalculateTemporaryImageTag(origImgRef)
   433  	if err != nil {
   434  		return image.EmptyName, err
   435  	}
   436  
   437  	// Apply the temporary tag to the current bundle to determine the new location for the image
   438  	newImgRef, err := bundleRef.WithTag(tmpImage.Tag())
   439  	if err != nil {
   440  		return image.EmptyName, err
   441  	}
   442  
   443  	// Convert it to the relocation library's representation of an image reference
   444  	return image.NewName(newImgRef.String())
   445  }
   446  
   447  func (p *Porter) rewriteBundleWithBundleImageDigest(ctx context.Context, m *manifest.Manifest, digest digest.Digest, preserveTags bool) (cnab.ExtendedBundle, error) {
   448  	taggedImage, err := p.rewriteImageWithDigest(m.Image, digest.String())
   449  	if err != nil {
   450  		return cnab.ExtendedBundle{}, fmt.Errorf("unable to update bundle image reference: %w", err)
   451  	}
   452  	m.Image = taggedImage
   453  
   454  	fmt.Fprintln(p.Out, "\nRewriting CNAB bundle.json...")
   455  	err = p.buildBundle(ctx, m, digest, preserveTags)
   456  	if err != nil {
   457  		return cnab.ExtendedBundle{}, fmt.Errorf("unable to rewrite CNAB bundle.json with updated bundle image digest: %w", err)
   458  	}
   459  
   460  	bun, err := cnab.LoadBundle(p.Context, build.LOCAL_BUNDLE)
   461  	if err != nil {
   462  		return cnab.ExtendedBundle{}, fmt.Errorf("unable to load CNAB bundle: %w", err)
   463  	}
   464  
   465  	return bun, nil
   466  }
   467  
   468  func (p *Porter) relocateImage(relocationMap relocation.ImageRelocationMap, layout registry.Layout, originImg string, newReference string) (relocation.ImageRelocationMap, error) {
   469  	newImgName, err := getNewImageNameFromBundleReference(originImg, newReference)
   470  	if err != nil {
   471  		return nil, err
   472  	}
   473  
   474  	originImgRef := originImg
   475  	if relocatedImage, ok := relocationMap[originImg]; ok {
   476  		originImgRef = relocatedImage
   477  	}
   478  	digest, err := pushUpdatedImage(layout, originImgRef, newImgName)
   479  	if err != nil {
   480  		return nil, fmt.Errorf("unable to push updated image: %w", err)
   481  	}
   482  
   483  	taggedImage, err := p.rewriteImageWithDigest(newImgName.String(), digest.String())
   484  	if err != nil {
   485  		return nil, fmt.Errorf("unable to update image reference for %s: %w", newImgName.String(), err)
   486  	}
   487  
   488  	// update relocation map
   489  	relocationMap[originImg] = taggedImage
   490  	return relocationMap, nil
   491  }
   492  
   493  func (p *Porter) rewriteImageWithDigest(image string, imgDigest string) (string, error) {
   494  	taggedRef, err := cnab.ParseOCIReference(image)
   495  	if err != nil {
   496  		return "", fmt.Errorf("unable to parse docker image: %s", err)
   497  	}
   498  
   499  	// Change the bundle image from bundlerepo:tag-hash => bundlerepo@sha256:abc123
   500  	// Do not continue to reference the temporary tag that we used to push, otherwise that will prevent the registry from garbage collecting it later.
   501  	repo := cnab.MustParseOCIReference(taggedRef.Repository())
   502  
   503  	digestedRef, err := repo.WithDigest(digest.Digest(imgDigest))
   504  	if err != nil {
   505  		return "", err
   506  	}
   507  	return digestedRef.String(), nil
   508  }
   509  
   510  // refreshCachedBundle will store a bundle anew, if a bundle with the same tag is found in the cache
   511  func (p *Porter) refreshCachedBundle(bundleRef cnab.BundleReference) error {
   512  	if _, found, _ := p.Cache.FindBundle(bundleRef.Reference); found {
   513  		_, err := p.Cache.StoreBundle(bundleRef)
   514  		if err != nil {
   515  			fmt.Fprintf(p.Err, "warning: unable to update cache for bundle %s: %s\n", bundleRef.Reference, err)
   516  		}
   517  	}
   518  	return nil
   519  }
   520  
   521  // signImage signs a image using the configured signing plugin
   522  func (p *Porter) signImage(ctx context.Context, ref cnab.OCIReference) error {
   523  	_, log := tracing.StartSpan(ctx)
   524  	defer log.EndSpan()
   525  
   526  	log.Debugf("Signing image %s...", ref.String())
   527  	return p.Signer.Sign(context.Background(), ref.String())
   528  }