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

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     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/manifest"
    16  	"get.porter.sh/porter/pkg/tracing"
    17  	"go.opentelemetry.io/otel/attribute"
    18  )
    19  
    20  // ensureLocalBundleIsUpToDate ensures that the bundle is up-to-date with the porter manifest,
    21  // if it is out-of-date, performs a build of the bundle.
    22  func (p *Porter) ensureLocalBundleIsUpToDate(ctx context.Context, opts BuildOptions) (cnab.BundleReference, error) {
    23  	ctx, log := tracing.StartSpan(ctx,
    24  		attribute.Bool("autobuild-disabled", opts.AutoBuildDisabled))
    25  	defer log.EndSpan()
    26  
    27  	if opts.File == "" {
    28  		return cnab.BundleReference{}, nil
    29  	}
    30  
    31  	upToDate, err := p.IsBundleUpToDate(ctx, opts.BundleDefinitionOptions)
    32  	if err != nil {
    33  		log.Warnf("WARNING: %w", err)
    34  	}
    35  
    36  	if !upToDate {
    37  		if opts.AutoBuildDisabled {
    38  			log.Warn("WARNING: The bundle is out-of-date. Skipping autobuild because --autobuild-disabled was specified")
    39  		} else {
    40  			log.Info("Changes have been detected and the previously built bundle is out-of-date, rebuilding the bundle before proceeding...")
    41  			log.Info("Building bundle ===>")
    42  			// opts.File is non-empty, which overrides opts.CNABFile if set
    43  			// (which may be if a cached bundle is fetched e.g. when running an action)
    44  			opts.CNABFile = ""
    45  			buildOpts := opts
    46  			if err = buildOpts.Validate(p); err != nil {
    47  				return cnab.BundleReference{}, log.Errorf("Validation of build options when autobuilding the bundle failed: %w", err)
    48  			}
    49  			err := p.Build(ctx, buildOpts)
    50  			if err != nil {
    51  				return cnab.BundleReference{}, err
    52  			}
    53  		}
    54  	}
    55  
    56  	bun, err := cnab.LoadBundle(p.Context, build.LOCAL_BUNDLE)
    57  	if err != nil {
    58  		if errors.Is(err, os.ErrNotExist) && opts.AutoBuildDisabled {
    59  			return cnab.BundleReference{}, log.Errorf("Attempted to use a bundle from source without building it first when --autobuild-disabled is set. Build the bundle and try again: %w", err)
    60  		}
    61  		return cnab.BundleReference{}, log.Error(err)
    62  	}
    63  
    64  	return cnab.BundleReference{
    65  		Definition: bun,
    66  	}, nil
    67  }
    68  
    69  // IsBundleUpToDate checks the hash of the manifest against the hash in cnab/bundle.json.
    70  func (p *Porter) IsBundleUpToDate(ctx context.Context, opts BundleDefinitionOptions) (bool, error) {
    71  	ctx, span := tracing.StartSpan(ctx)
    72  	defer span.EndSpan()
    73  
    74  	span.Debugf("Checking if the bundle is up-to-date...")
    75  
    76  	// This is a prefix for any message that explains why the bundle is out-of-date
    77  	const rebuildMessagePrefix = "Bundle is out-of-date and must be rebuilt"
    78  
    79  	if opts.File == "" {
    80  		span.Debugf("%s because the current bundle was not specified. Please report this as a bug!", rebuildMessagePrefix)
    81  		return false, span.Errorf("File is required")
    82  	}
    83  	m, err := manifest.LoadManifestFrom(ctx, p.Config, opts.File)
    84  	if err != nil {
    85  		err = fmt.Errorf("the current bundle could not be read: %w", err)
    86  		span.Debugf("%s: %w", rebuildMessagePrefix, err)
    87  		return false, span.Error(err)
    88  	}
    89  
    90  	if exists, _ := p.FileSystem.Exists(opts.CNABFile); exists {
    91  		bun, err := cnab.LoadBundle(p.Context, opts.CNABFile)
    92  		if err != nil {
    93  			err = fmt.Errorf("the previously built bundle at %s could not be read: %w", opts.CNABFile, err)
    94  			span.Debugf("%s: %w", rebuildMessagePrefix, err)
    95  			return false, span.Error(err)
    96  		}
    97  
    98  		// Check whether bundle images exist in host registry.
    99  		for _, invocationImage := range bun.InvocationImages {
   100  			// if the invocationImage is built before using a random string tag,
   101  			// we should rebuild it with the new format
   102  			if strings.HasSuffix(invocationImage.Image, "-installer") {
   103  				span.Debugf("%s because it uses the old -installer suffixed image name (%s)", invocationImage.Image)
   104  				return false, nil
   105  			}
   106  
   107  			imgRef, err := cnab.ParseOCIReference(invocationImage.Image)
   108  			if err != nil {
   109  				err = fmt.Errorf("error parsing %s as an OCI image reference: %w", invocationImage.Image, err)
   110  				span.Debugf("%s: %w", rebuildMessagePrefix, err)
   111  				return false, span.Error(err)
   112  			}
   113  
   114  			_, err = p.Registry.GetCachedImage(ctx, imgRef)
   115  			if err != nil {
   116  				if errors.Is(err, cnabtooci.ErrNotFound{}) {
   117  					span.Debugf("%s because the bundle image %s doesn't exist in the local image cache", rebuildMessagePrefix, invocationImage.Image)
   118  					return false, nil
   119  				}
   120  				err = fmt.Errorf("an error occurred checking the Docker cache for the bundle image: %w", err)
   121  				span.Debugf("%s: %w", rebuildMessagePrefix, err)
   122  				return false, span.Error(err)
   123  			}
   124  		}
   125  
   126  		oldStamp, err := configadapter.LoadStamp(bun)
   127  		if err != nil {
   128  			err = fmt.Errorf("could not load stamp from %s: %w", opts.CNABFile, err)
   129  			span.Debugf("%s: %w", rebuildMessagePrefix)
   130  			return false, span.Error(err)
   131  		}
   132  
   133  		mixins, err := p.getUsedMixins(ctx, m)
   134  		if err != nil {
   135  			err = fmt.Errorf("an error occurred while listing used mixins: %w", err)
   136  			span.Debugf("%s: %w", rebuildMessagePrefix, err)
   137  			return false, span.Error(err)
   138  		}
   139  
   140  		converter := configadapter.NewManifestConverter(p.Config, m, nil, mixins, opts.PreserveTags)
   141  		newDigest, err := converter.DigestManifest()
   142  		if err != nil {
   143  			err = fmt.Errorf("the current manifest digest cannot be calculated: %w", err)
   144  			span.Debugf("%s: %w", rebuildMessagePrefix, err)
   145  			return false, span.Error(err)
   146  		}
   147  
   148  		preserveTagsChanged := oldStamp.PreserveTags != opts.PreserveTags
   149  		digestChanged := oldStamp.ManifestDigest != newDigest
   150  		manifestChanged := digestChanged || preserveTagsChanged
   151  		if manifestChanged {
   152  			if preserveTagsChanged {
   153  				span.Debugf("PreserveTags is set to %t in the stamp, but the build is being run with PreserveTags set to %t", oldStamp.PreserveTags, opts.PreserveTags)
   154  			}
   155  			if digestChanged {
   156  				span.Debugf("%s because the cached bundle is stale", rebuildMessagePrefix)
   157  			}
   158  			if span.IsTracingEnabled() {
   159  				previousStampB, _ := json.Marshal(oldStamp)
   160  				currentStamp, _ := converter.GenerateStamp(ctx, opts.PreserveTags)
   161  				currentStampB, _ := json.Marshal(currentStamp)
   162  				span.SetAttributes(
   163  					attribute.String("previous-stamp", string(previousStampB)),
   164  					attribute.String("current-stamp", string(currentStampB)),
   165  				)
   166  			}
   167  			return false, nil
   168  		}
   169  
   170  		span.Debugf("Bundle is up-to-date!")
   171  		return true, nil
   172  	}
   173  
   174  	span.Debugf("%s because a previously built bundle was not found", rebuildMessagePrefix)
   175  	return false, nil
   176  }