get.porter.sh/porter@v1.3.0/pkg/cnab/config-adapter/stamp.go (about)

     1  package configadapter
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"sort"
    12  
    13  	"get.porter.sh/porter/pkg"
    14  	"get.porter.sh/porter/pkg/cnab"
    15  	"get.porter.sh/porter/pkg/config"
    16  	"get.porter.sh/porter/pkg/manifest"
    17  	"get.porter.sh/porter/pkg/portercontext"
    18  	"get.porter.sh/porter/pkg/tracing"
    19  	"github.com/Masterminds/semver/v3"
    20  )
    21  
    22  // Stamp contains Porter specific metadata about a bundle that we can place
    23  // in the custom section of a bundle.json
    24  type Stamp struct {
    25  	// ManifestDigest takes into account all unique data that goes into a
    26  	// porter build to help determine if the last build is stale.
    27  	// * manifest
    28  	// * mixins
    29  	// * (TODO) files in current directory
    30  	ManifestDigest string `json:"manifestDigest"`
    31  
    32  	// Mixins used in the bundle.
    33  	Mixins map[string]MixinRecord `json:"mixins"`
    34  
    35  	// Manifest is the base64 encoded porter.yaml.
    36  	EncodedManifest string `json:"manifest"`
    37  
    38  	// Version and commit define the version of the Porter used when a bundle was built.
    39  	Version      string `json:"version"`
    40  	Commit       string `json:"commit"`
    41  	PreserveTags bool   `json:"preserveTags"`
    42  }
    43  
    44  // DecodeManifest base64 decodes the manifest stored in the stamp
    45  func (s Stamp) DecodeManifest() ([]byte, error) {
    46  	if s.EncodedManifest == "" {
    47  		return nil, errors.New("no Porter manifest was embedded in the bundle")
    48  	}
    49  
    50  	resultB, err := base64.StdEncoding.DecodeString(s.EncodedManifest)
    51  	if err != nil {
    52  		return nil, fmt.Errorf("could not base64 decode the manifest in the stamp\n%s: %w", s.EncodedManifest, err)
    53  	}
    54  
    55  	return resultB, nil
    56  }
    57  
    58  func (s Stamp) WriteManifest(cxt *portercontext.Context, path string) error {
    59  	manifestB, err := s.DecodeManifest()
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	err = cxt.FileSystem.WriteFile(path, manifestB, pkg.FileModeWritable)
    65  	if err != nil {
    66  		return fmt.Errorf("could not save decoded manifest to %s: %w", path, err)
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  // MixinRecord contains information about a mixin used in a bundle
    73  // For now it is a placeholder for data that we would like to include in the future.
    74  type MixinRecord struct {
    75  	// Name of the mixin used in the bundle. This is used for sorting only, and
    76  	// should not be written to the Porter's stamp in bundle.json because we are
    77  	// storing these mixin records in a map, keyed by the mixin name.
    78  	Name string `json:"-"`
    79  
    80  	// Version of the mixin used in the bundle.
    81  	Version string `json:"version"`
    82  }
    83  
    84  type MixinRecords []MixinRecord
    85  
    86  func (m MixinRecords) Len() int {
    87  	return len(m)
    88  }
    89  
    90  func (m MixinRecords) Less(i, j int) bool {
    91  	// Currently there can only be a single version of a mixin used in a bundle
    92  	// I'm considering version as well for sorting in case that changes in the future once mixins are bundles
    93  	// referenced by a bundle, and not embedded binaries
    94  	iRecord := m[i]
    95  	jRecord := m[j]
    96  	if iRecord.Name == jRecord.Name {
    97  		// Try to sort by the mixin's semantic version
    98  		// If it doesn't parse, just fall through and sort as a string instead
    99  		iVersion, iErr := semver.NewVersion(iRecord.Version)
   100  		jVersion, jErr := semver.NewVersion(jRecord.Version)
   101  		if iErr == nil && jErr == nil {
   102  			return iVersion.LessThan(jVersion)
   103  		} else {
   104  			return iRecord.Version < jRecord.Version
   105  		}
   106  	}
   107  
   108  	return iRecord.Name < jRecord.Name
   109  }
   110  
   111  func (m MixinRecords) Swap(i, j int) {
   112  	tmp := m[i]
   113  	m[i] = m[j]
   114  	m[j] = tmp
   115  }
   116  
   117  func (c *ManifestConverter) GenerateStamp(ctx context.Context, preserveTags bool) (Stamp, error) {
   118  	log := tracing.LoggerFromContext(ctx)
   119  
   120  	stamp := Stamp{}
   121  
   122  	// Remember the original porter.yaml, base64 encoded to avoid canonical json shenanigans
   123  	rawManifest, err := manifest.ReadManifestData(c.config.Context, c.Manifest.ManifestPath)
   124  	if err != nil {
   125  		return Stamp{}, err
   126  	}
   127  	stamp.EncodedManifest = base64.StdEncoding.EncodeToString(rawManifest)
   128  	stamp.PreserveTags = preserveTags
   129  
   130  	stamp.Mixins = make(map[string]MixinRecord, len(c.Manifest.Mixins))
   131  	usedMixins := c.getUsedMixinRecords()
   132  	for _, record := range usedMixins {
   133  		stamp.Mixins[record.Name] = record
   134  	}
   135  
   136  	digest, err := c.DigestManifest()
   137  	if err != nil {
   138  		// The digest is only used to decide if we need to rebuild, it is not an error condition to not
   139  		// have a digest.
   140  		log.Warn(fmt.Sprint("WARNING: Could not digest the porter manifest file: %w", err))
   141  		stamp.ManifestDigest = "unknown"
   142  	} else {
   143  		stamp.ManifestDigest = digest
   144  	}
   145  
   146  	stamp.Version = pkg.Version
   147  	stamp.Commit = pkg.Commit
   148  
   149  	return stamp, nil
   150  }
   151  
   152  func (c *ManifestConverter) DigestManifest() (string, error) {
   153  	if exists, _ := c.config.FileSystem.Exists(c.Manifest.ManifestPath); !exists {
   154  		return "", fmt.Errorf("the specified porter configuration file %s does not exist", c.Manifest.ManifestPath)
   155  	}
   156  
   157  	data, err := c.config.FileSystem.ReadFile(c.Manifest.ManifestPath)
   158  	if err != nil {
   159  		return "", fmt.Errorf("could not read manifest at %q: %w", c.Manifest.ManifestPath, err)
   160  	}
   161  
   162  	v := pkg.Version
   163  	data = append(data, v...)
   164  
   165  	usedMixins := c.getUsedMixinRecords()
   166  	sort.Sort(usedMixins) // Ensure that this is sorted so the digest is consistent
   167  	for _, mixinRecord := range usedMixins {
   168  		data = append(append(data, mixinRecord.Name...), mixinRecord.Version...)
   169  	}
   170  
   171  	digest := sha256.Sum256(data)
   172  	return hex.EncodeToString(digest[:]), nil
   173  }
   174  
   175  func LoadStamp(bun cnab.ExtendedBundle) (Stamp, error) {
   176  	// TODO(carolynvs): can we simplify some of this by using the extended bundle?
   177  	data, ok := bun.Custom[config.CustomPorterKey]
   178  	if !ok {
   179  		return Stamp{}, fmt.Errorf("porter stamp (custom.%s) was not present on the bundle", config.CustomPorterKey)
   180  	}
   181  
   182  	dataB, err := json.Marshal(data)
   183  	if err != nil {
   184  		return Stamp{}, fmt.Errorf("could not marshal the porter stamp %q: %w", string(dataB), err)
   185  	}
   186  
   187  	stamp := Stamp{}
   188  	err = json.Unmarshal(dataB, &stamp)
   189  	if err != nil {
   190  		return Stamp{}, fmt.Errorf("could not unmarshal the porter stamp %q: %w", string(dataB), err)
   191  	}
   192  
   193  	return stamp, nil
   194  }
   195  
   196  // getUsedMixinRecords returns a list of the mixins used by the bundle, including
   197  // information about the installed mixin, such as its version.
   198  func (c *ManifestConverter) getUsedMixinRecords() MixinRecords {
   199  	usedMixins := make(MixinRecords, 0)
   200  
   201  	for _, usedMixin := range c.Manifest.Mixins {
   202  		for _, installedMixin := range c.InstalledMixins {
   203  			if usedMixin.Name == installedMixin.Name {
   204  				usedMixins = append(usedMixins, MixinRecord{
   205  					Name:    installedMixin.Name,
   206  					Version: installedMixin.GetVersionInfo().Version,
   207  				})
   208  			}
   209  		}
   210  	}
   211  
   212  	sort.Sort(usedMixins)
   213  	return usedMixins
   214  }