get.porter.sh/porter@v1.3.0/pkg/cnab/extended_bundle.go (about) 1 package cnab 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 8 depsv1ext "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v1" 9 v2 "get.porter.sh/porter/pkg/cnab/extensions/dependencies/v2" 10 "get.porter.sh/porter/pkg/portercontext" 11 "get.porter.sh/porter/pkg/schema" 12 "github.com/Masterminds/semver/v3" 13 "github.com/cnabio/cnab-go/bundle" 14 "github.com/cnabio/cnab-go/bundle/definition" 15 "github.com/cnabio/cnab-go/claim" 16 "github.com/google/go-containerregistry/pkg/crane" 17 ) 18 19 const SupportedVersion = "1.0.0 || 1.1.0 || 1.2.0" 20 21 var DefaultSchemaVersion = semver.MustParse(string(BundleSchemaVersion())) 22 23 // ExtendedBundle is a bundle that has typed access to extensions declared in the bundle, 24 // allowing quick type-safe access to custom extensions from the CNAB spec. 25 type ExtendedBundle struct { 26 bundle.Bundle 27 } 28 29 type DependencyLock struct { 30 Alias string 31 Reference string 32 SharingMode bool 33 SharingGroup string 34 } 35 36 // NewBundle creates an ExtendedBundle from a given bundle. 37 func NewBundle(bundle bundle.Bundle) ExtendedBundle { 38 return ExtendedBundle{bundle} 39 } 40 41 // LoadBundle from the specified filepath. 42 func LoadBundle(c *portercontext.Context, bundleFile string) (ExtendedBundle, error) { 43 bunD, err := c.FileSystem.ReadFile(bundleFile) 44 if err != nil { 45 return ExtendedBundle{}, fmt.Errorf("cannot read bundle at %s: %w", bundleFile, err) 46 } 47 48 bun, err := bundle.Unmarshal(bunD) 49 if err != nil { 50 return ExtendedBundle{}, fmt.Errorf("cannot load bundle from\n%s at %s: %w", string(bunD), bundleFile, err) 51 } 52 53 return NewBundle(*bun), nil 54 } 55 56 func (b ExtendedBundle) Validate(cxt *portercontext.Context, strategy schema.CheckStrategy) error { 57 err := b.Bundle.Validate() 58 if err != nil { 59 return fmt.Errorf("invalid bundle: %w", err) 60 } 61 62 supported, err := semver.NewConstraint(SupportedVersion) 63 if err != nil { 64 return fmt.Errorf("invalid supported version %s: %w", SupportedVersion, err) 65 } 66 isWarn, err := schema.ValidateSchemaVersion(strategy, supported, string(b.SchemaVersion), DefaultSchemaVersion) 67 if err != nil && !isWarn { 68 return err 69 } 70 71 if isWarn { 72 fmt.Fprintln(cxt.Err, err) 73 } 74 75 return nil 76 } 77 78 // IsPorterBundle determines if the bundle was created by Porter. 79 func (b ExtendedBundle) IsPorterBundle() bool { 80 _, madeByPorter := b.Custom[PorterExtension] 81 return madeByPorter 82 } 83 84 // IsInternalParameter determines if the provided parameter is internal 85 // to Porter after analyzing the provided bundle. 86 func (b ExtendedBundle) IsInternalParameter(name string) bool { 87 if param, exists := b.Parameters[name]; exists { 88 if def, exists := b.Definitions[param.Definition]; exists { 89 return def.Comment == PorterInternal 90 } 91 } 92 return false 93 } 94 95 // IsInternalOutput determines if the provided output is internal 96 // to Porter after analyzing the provided bundle. 97 func (b ExtendedBundle) IsInternalOutput(name string) bool { 98 if output, exists := b.Outputs[name]; exists { 99 if def, exists := b.Definitions[output.Definition]; exists { 100 return def.Comment == PorterInternal 101 } 102 } 103 return false 104 } 105 106 // IsSensitiveParameter determines if the parameter contains a sensitive value. 107 func (b ExtendedBundle) IsSensitiveParameter(param string) bool { 108 if param, exists := b.Parameters[param]; exists { 109 if def, exists := b.Definitions[param.Definition]; exists { 110 return def.WriteOnly != nil && *def.WriteOnly 111 } 112 } 113 return false 114 } 115 116 // GetParameterType determines the type of parameter accounting for 117 // Porter-specific parameter types like file. 118 func (b ExtendedBundle) GetParameterType(def *definition.Schema) string { 119 if b.IsFileType(def) { 120 return "file" 121 } 122 123 if def.ID == claim.OutputInvocationImageLogs { 124 return "string" 125 } 126 127 return fmt.Sprintf("%v", def.Type) 128 } 129 130 // IsFileType determines if the parameter/credential is of type "file". 131 func (b ExtendedBundle) IsFileType(def *definition.Schema) bool { 132 return b.SupportsFileParameters() && 133 def.Type == "string" && def.ContentEncoding == "base64" 134 } 135 136 // ConvertParameterValue converts a parameter's value from an unknown type, 137 // it could be a string from stdin or another Go type, into the type of the 138 // parameter as defined in the bundle. 139 func (b ExtendedBundle) ConvertParameterValue(key string, value interface{}) (interface{}, error) { 140 param, ok := b.Parameters[key] 141 if !ok { 142 return nil, fmt.Errorf("unable to convert the parameters' value to the destination parameter type because parameter %s not defined in bundle", key) 143 } 144 145 def, ok := b.Definitions[param.Definition] 146 if !ok { 147 return nil, fmt.Errorf("unable to convert the parameters' value to the destination parameter type because parameter %s has no definition", key) 148 } 149 150 if def.Type != nil { 151 switch t := value.(type) { 152 case string: 153 typedValue, err := def.ConvertValue(t) 154 if err != nil { 155 return nil, fmt.Errorf("unable to convert parameter's %s value %s to the destination parameter type %s: %w", key, value, def.Type, err) 156 } 157 return typedValue, nil 158 case json.Number: 159 switch def.Type { 160 case "integer": 161 return t.Int64() 162 case "number": 163 return t.Float64() 164 default: 165 return t.String(), nil 166 } 167 default: 168 return t, nil 169 } 170 } else { 171 return value, nil 172 } 173 } 174 175 func (b ExtendedBundle) WriteParameterToString(paramName string, value interface{}) (string, error) { 176 return WriteParameterToString(paramName, value) 177 } 178 179 // WriteParameterToString changes a parameter's value from its type as 180 // defined by the bundle to its runtime string representation. 181 // The value should have already been converted to its bundle representation 182 // by calling ConvertParameterValue. 183 func WriteParameterToString(paramName string, value interface{}) (string, error) { 184 if value == nil { 185 return "", nil 186 } 187 188 if stringVal, ok := value.(string); ok { 189 return stringVal, nil 190 } 191 192 contents, err := json.Marshal(value) 193 if err != nil { 194 return "", fmt.Errorf("could not marshal the value for parameter %s to a json string %#v: %w", paramName, value, err) 195 } 196 197 return string(contents), nil 198 } 199 200 // GetReferencedRegistries identifies all OCI registries used by the bundle 201 // from both the bundle image and the referenced images. 202 func (b ExtendedBundle) GetReferencedRegistries() ([]string, error) { 203 regMap := make(map[string]struct{}) 204 for _, ii := range b.InvocationImages { 205 imgRef, err := ParseOCIReference(ii.Image) 206 if err != nil { 207 return nil, fmt.Errorf("could not parse the bundle image %s as an OCI image reference: %w", ii.Image, err) 208 } 209 210 regMap[imgRef.Registry()] = struct{}{} 211 } 212 213 for key, img := range b.Images { 214 imgRef, err := ParseOCIReference(img.Image) 215 if err != nil { 216 return nil, fmt.Errorf("could not parse the referenced image %s (%s) as an OCI image reference: %w", img.Image, key, err) 217 } 218 regMap[imgRef.Registry()] = struct{}{} 219 } 220 221 regs := make([]string, 0, len(regMap)) 222 for reg := range regMap { 223 regs = append(regs, reg) 224 } 225 sort.Strings(regs) 226 return regs, nil 227 } 228 229 func (b *ExtendedBundle) ResolveDependencies(bun ExtendedBundle) ([]DependencyLock, error) { 230 if bun.HasDependenciesV2() { 231 return b.ResolveSharedDeps(bun) 232 } 233 234 if !bun.HasDependenciesV1() { 235 return nil, nil 236 } 237 rawDeps, err := bun.ReadDependenciesV1() 238 // We need make sure the DependenciesV1 are ordered by the desired sequence 239 orderedDeps := rawDeps.ListBySequence() 240 241 if err != nil { 242 return nil, fmt.Errorf("error executing dependencies for %s: %w", bun.Name, err) 243 } 244 245 q := make([]DependencyLock, 0, len(orderedDeps)) 246 for _, dep := range orderedDeps { 247 ref, err := b.ResolveVersion(dep.Name, dep) 248 if err != nil { 249 return nil, err 250 } 251 252 lock := DependencyLock{ 253 Alias: dep.Name, 254 Reference: ref.String(), 255 SharingMode: false, 256 } 257 q = append(q, lock) 258 } 259 260 return q, nil 261 } 262 263 // ResolveSharedDeps only works with depsv2 264 func (b *ExtendedBundle) ResolveSharedDeps(bun ExtendedBundle) ([]DependencyLock, error) { 265 v2, err := bun.ReadDependenciesV2() 266 if err != nil { 267 return nil, fmt.Errorf("error reading dependencies v2 for %s", bun.Name) 268 } 269 270 q := make([]DependencyLock, 0, len(v2.Requires)) 271 for name, d := range v2.Requires { 272 d.Name = name 273 274 if d.Sharing.Mode && d.Sharing.Group.Name == "" { 275 return nil, fmt.Errorf("empty sharing group, sharing group name needs to be specified to be active") 276 } 277 if !d.Sharing.Mode && d.Sharing.Group.Name != "" { 278 return nil, fmt.Errorf("empty sharing mode, sharing mode boolean set to `true` to be active") 279 } 280 281 ref, err := b.ResolveVersionv2(d.Name, d) 282 if err != nil { 283 return nil, err 284 } 285 286 lock := DependencyLock{ 287 Alias: d.Name, 288 Reference: ref.String(), 289 SharingMode: d.Sharing.Mode, 290 SharingGroup: d.Sharing.Group.Name, 291 } 292 q = append(q, lock) 293 } 294 return q, nil 295 } 296 297 // ResolveVersion returns the bundle name, its version and any error. 298 func (b *ExtendedBundle) ResolveVersion(name string, dep depsv1ext.Dependency) (OCIReference, error) { 299 ref, err := ParseOCIReference(dep.Bundle) 300 if err != nil { 301 return OCIReference{}, fmt.Errorf("error parsing dependency (%s) bundle %q as OCI reference: %w", name, dep.Bundle, err) 302 } 303 304 // Here is where we could split out this logic into multiple strategy funcs / structs if necessary 305 if dep.Version == nil || len(dep.Version.Ranges) == 0 { 306 // Check if they specified an explicit tag in referenced bundle already 307 if ref.HasTag() { 308 return ref, nil 309 } 310 311 tag, err := b.determineDefaultTag(dep) 312 if err != nil { 313 return OCIReference{}, err 314 } 315 316 return ref.WithTag(tag) 317 } 318 319 return OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err) 320 } 321 322 func (b *ExtendedBundle) determineDefaultTag(dep depsv1ext.Dependency) (string, error) { 323 tags, err := crane.ListTags(dep.Bundle) 324 if err != nil { 325 return "", fmt.Errorf("error listing tags for %s: %w", dep.Bundle, err) 326 } 327 328 allowPrereleases := false 329 if dep.Version != nil && dep.Version.AllowPrereleases { 330 allowPrereleases = true 331 } 332 333 var hasLatest bool 334 versions := make(semver.Collection, 0, len(tags)) 335 for _, tag := range tags { 336 if tag == "latest" { 337 hasLatest = true 338 continue 339 } 340 341 version, err := semver.NewVersion(tag) 342 if err == nil { 343 if !allowPrereleases && version.Prerelease() != "" { 344 continue 345 } 346 versions = append(versions, version) 347 } 348 } 349 350 if len(versions) == 0 { 351 if hasLatest { 352 return "latest", nil 353 } else { 354 return "", fmt.Errorf("no tag was specified for %s and none of the tags defined in the registry meet the criteria: semver formatted or 'latest'", dep.Bundle) 355 } 356 } 357 358 sort.Sort(sort.Reverse(versions)) 359 360 return versions[0].Original(), nil 361 } 362 363 // BuildPrerequisiteInstallationName generates the name of a prerequisite dependency installation. 364 func (b *ExtendedBundle) BuildPrerequisiteInstallationName(installation string, dependency string) string { 365 return fmt.Sprintf("%s-%s", installation, dependency) 366 } 367 368 // this is all copied v2 stuff 369 // todo(schristoff): in the future, we should clean this up 370 371 // ResolveVersion returns the bundle name, its version and any error. 372 func (b *ExtendedBundle) ResolveVersionv2(name string, dep v2.Dependency) (OCIReference, error) { 373 ref, err := ParseOCIReference(dep.Bundle) 374 if err != nil { 375 return OCIReference{}, fmt.Errorf("error parsing dependency (%s) bundle %q as OCI reference: %w", name, dep.Bundle, err) 376 } 377 378 if dep.Version == "" { 379 // Check if they specified an explicit tag or digest in referenced bundle already 380 if ref.HasTag() || ref.HasDigest() { 381 return ref, nil 382 } 383 384 tag, err := b.determineDefaultTagv2(dep) 385 if err != nil { 386 return OCIReference{}, err 387 } 388 389 return ref.WithTag(tag) 390 } 391 //I think this is going to need to be smarter 392 if dep.Version != "" { 393 return ref, nil 394 } 395 396 return OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err) 397 } 398 399 func (b *ExtendedBundle) determineDefaultTagv2(dep v2.Dependency) (string, error) { 400 tags, err := crane.ListTags(dep.Bundle) 401 if err != nil { 402 return "", fmt.Errorf("error listing tags for %s: %w", dep.Bundle, err) 403 } 404 405 var hasLatest bool 406 versions := make(semver.Collection, 0, len(tags)) 407 for _, tag := range tags { 408 if tag == "latest" { 409 hasLatest = true 410 continue 411 } 412 413 } 414 if len(versions) == 0 { 415 if hasLatest { 416 return "latest", nil 417 } else { 418 return "", fmt.Errorf("no tag was specified for %s and none of the tags defined in the registry meet the criteria: semver formatted or 'latest'", dep.Bundle) 419 } 420 } 421 422 return versions[0].Original(), nil 423 }