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 }