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  }