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

     1  package manifest
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"path"
    10  	"reflect"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  
    15  	"get.porter.sh/porter/pkg/cnab"
    16  	"get.porter.sh/porter/pkg/config"
    17  	"get.porter.sh/porter/pkg/experimental"
    18  	"get.porter.sh/porter/pkg/portercontext"
    19  	"get.porter.sh/porter/pkg/schema"
    20  	"get.porter.sh/porter/pkg/tracing"
    21  	"get.porter.sh/porter/pkg/yaml"
    22  	"github.com/Masterminds/semver/v3"
    23  	"github.com/cbroglie/mustache"
    24  	"github.com/cnabio/cnab-go/bundle"
    25  	"github.com/cnabio/cnab-go/bundle/definition"
    26  	"github.com/dustin/go-humanize"
    27  	"github.com/hashicorp/go-multierror"
    28  	"github.com/opencontainers/go-digest"
    29  	"go.opentelemetry.io/otel/attribute"
    30  )
    31  
    32  const (
    33  	invalidStepErrorFormat = "validation of action \"%s\" failed: %w"
    34  
    35  	// SchemaTypeBundle is the default schemaType value for Bundle resources
    36  	SchemaTypeBundle = "Bundle"
    37  
    38  	// TemplateDelimiterPrefix must be present at the beginning of any porter.yaml
    39  	// that wants to use ${} as the template delimiter instead of the mustache
    40  	// default of {{}}.
    41  	TemplateDelimiterPrefix = "{{=${ }=}}\n"
    42  )
    43  
    44  var (
    45  	// TODO(PEP003): Version 1.1.0 is behind the DependenciesV2 feature flag. We update the supported versions later
    46  	// when validating a bundle and only allow that version when it is enabled.
    47  	// The default version remains on the last stable version 1.0.1.
    48  	// When the schema version is stable and not behind a feature flag, we can update the supported versions and default version.
    49  
    50  	// SupportedSchemaVersions is the Porter manifest (porter.yaml) schema
    51  	// versions supported by this version of Porter, specified as a semver range.
    52  	// When the Manifest structure is changed, this field should be incremented.
    53  	SupportedSchemaVersions, _ = semver.NewConstraint("1.0.0-alpha.1 || 1.0.0 - 1.0.1")
    54  
    55  	// DefaultSchemaVersion is the most recently supported schema version.
    56  	// When the Manifest structure is changed, this field should be incremented.
    57  	DefaultSchemaVersion = semver.MustParse("1.0.1")
    58  )
    59  
    60  type Manifest struct {
    61  	// ManifestPath is location to the original, user-supplied manifest, such as the path on the filesystem or a url
    62  	ManifestPath string `yaml:"-"`
    63  
    64  	// TemplateVariables are the variables used in the templating, e.g. bundle.parameters.NAME, or bundle.outputs.NAME
    65  	TemplateVariables []string `yaml:"-"`
    66  
    67  	// SchemaType indicates the type of resource contained in an imported file.
    68  	SchemaType string `yaml:"schemaType,omitempty"`
    69  
    70  	// SchemaVersion is a semver value that indicates which version of the porter.yaml schema is used in the file.
    71  	SchemaVersion string `yaml:"schemaVersion"`
    72  	Name          string `yaml:"name,omitempty"`
    73  	Description   string `yaml:"description,omitempty"`
    74  	Version       string `yaml:"version,omitempty"`
    75  
    76  	Maintainers []MaintainerDefinition `yaml:"maintainers,omitempty"`
    77  
    78  	// Registry is the OCI registry and org/subdomain for the bundle
    79  	Registry string `yaml:"registry,omitempty"`
    80  
    81  	// Reference is the optional, full bundle reference
    82  	// in the format REGISTRY/NAME or REGISTRY/NAME:TAG
    83  	Reference string `yaml:"reference,omitempty"`
    84  
    85  	// DockerTag is the Docker tag portion of the published bundle
    86  	// image and bundle.  It will only be set at time of publishing.
    87  	DockerTag string `yaml:"-"`
    88  
    89  	// Image is the name of the bundle image in the format REGISTRY/NAME:TAG
    90  	// It doesn't map to any field in the manifest as it has been deprecated
    91  	// and isn't meant to be user-specified
    92  	Image string `yaml:"-"`
    93  
    94  	// Dockerfile is the relative path to the Dockerfile template for the bundle image
    95  	Dockerfile string `yaml:"dockerfile,omitempty"`
    96  
    97  	Mixins []MixinDeclaration `yaml:"mixins,omitempty"`
    98  
    99  	Install   Steps `yaml:"install"`
   100  	Uninstall Steps `yaml:"uninstall"`
   101  	Upgrade   Steps `yaml:"upgrade"`
   102  
   103  	Custom                  CustomDefinitions                 `yaml:"custom,omitempty"`
   104  	CustomActions           map[string]Steps                  `yaml:"-"`
   105  	CustomActionDefinitions map[string]CustomActionDefinition `yaml:"customActions,omitempty"`
   106  
   107  	StateBag     StateBag              `yaml:"state,omitempty"`
   108  	Parameters   ParameterDefinitions  `yaml:"parameters,omitempty"`
   109  	Credentials  CredentialDefinitions `yaml:"credentials,omitempty"`
   110  	Dependencies Dependencies          `yaml:"dependencies,omitempty"`
   111  	Outputs      OutputDefinitions     `yaml:"outputs,omitempty"`
   112  
   113  	// ImageMap is a map of images referenced in the bundle. If an image relocation mapping is later provided, that
   114  	// will be mounted at as a file at runtime to /cnab/app/relocation-mapping.json.
   115  	ImageMap map[string]MappedImage `yaml:"images,omitempty"`
   116  
   117  	Required []RequiredExtension `yaml:"required,omitempty"`
   118  }
   119  
   120  func (m *Manifest) Validate(ctx context.Context, cfg *config.Config) error {
   121  	ctx, span := tracing.StartSpan(ctx)
   122  	defer span.EndSpan()
   123  
   124  	var result error
   125  
   126  	err := m.validateMetadata(ctx, cfg)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	err = m.SetDefaults()
   132  	if err != nil {
   133  		return span.Error(err)
   134  	}
   135  
   136  	if strings.ToLower(m.Dockerfile) == "dockerfile" {
   137  		return span.Error(errors.New("Dockerfile template cannot be named 'Dockerfile' because that is the filename generated during porter build"))
   138  	}
   139  
   140  	if len(m.Mixins) == 0 {
   141  		result = multierror.Append(result, errors.New("no mixins declared"))
   142  	}
   143  
   144  	if m.Install == nil {
   145  		result = multierror.Append(result, errors.New("no install action defined"))
   146  	}
   147  	err = m.Install.Validate(m)
   148  	if err != nil {
   149  		result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "install", err))
   150  	}
   151  
   152  	if m.Uninstall == nil {
   153  		result = multierror.Append(result, errors.New("no uninstall action defined"))
   154  	}
   155  	err = m.Uninstall.Validate(m)
   156  	if err != nil {
   157  		result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "uninstall", err))
   158  	}
   159  
   160  	if m.Upgrade != nil {
   161  		err = m.Upgrade.Validate(m)
   162  		if err != nil {
   163  			result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, "upgrade", err))
   164  		}
   165  	}
   166  
   167  	for actionName, steps := range m.CustomActions {
   168  		err := steps.Validate(m)
   169  		if err != nil {
   170  			result = multierror.Append(result, fmt.Errorf(invalidStepErrorFormat, actionName, err))
   171  		}
   172  	}
   173  
   174  	for _, dep := range m.Dependencies.Requires {
   175  		err = dep.Validate(cfg.Context)
   176  		if err != nil {
   177  			result = multierror.Append(result, err)
   178  		}
   179  	}
   180  
   181  	for _, output := range m.Outputs {
   182  		err = output.Validate()
   183  		if err != nil {
   184  			result = multierror.Append(result, err)
   185  		}
   186  	}
   187  
   188  	for _, parameter := range m.Parameters {
   189  		err = parameter.Validate()
   190  		if err != nil {
   191  			result = multierror.Append(result, err)
   192  		}
   193  	}
   194  
   195  	for _, image := range m.ImageMap {
   196  		err = image.Validate()
   197  		if err != nil {
   198  			result = multierror.Append(result, err)
   199  		}
   200  	}
   201  
   202  	return span.Error(result)
   203  }
   204  
   205  func (m *Manifest) validateMetadata(ctx context.Context, cfg *config.Config) error {
   206  	ctx, span := tracing.StartSpan(ctx)
   207  	defer span.EndSpan()
   208  
   209  	strategy := cfg.GetSchemaCheckStrategy(ctx)
   210  	span.SetAttributes(attribute.String("schemaCheckStrategy", string(strategy)))
   211  
   212  	if m.SchemaType == "" {
   213  		m.SchemaType = SchemaTypeBundle
   214  	} else if !strings.EqualFold(m.SchemaType, SchemaTypeBundle) {
   215  		return span.Errorf("invalid schemaType %s, expected %s", m.SchemaType, SchemaTypeBundle)
   216  	}
   217  
   218  	// Check what the supported schema version is based on if depsv2 is enabled
   219  	supportedVersions := SupportedSchemaVersions
   220  	if cfg.IsFeatureEnabled(experimental.FlagDependenciesV2) {
   221  		supportedVersions, _ = semver.NewConstraint("1.0.0-alpha.1 || 1.0.0 - 1.0.1 || 1.1.0")
   222  	}
   223  
   224  	if warnOnly, err := schema.ValidateSchemaVersion(strategy, supportedVersions, m.SchemaVersion, DefaultSchemaVersion); err != nil {
   225  		if warnOnly {
   226  			span.Warn(err.Error())
   227  		} else {
   228  			return span.Error(err)
   229  		}
   230  	}
   231  
   232  	if m.Name == "" {
   233  		return span.Error(errors.New("bundle name must be set"))
   234  	}
   235  
   236  	if m.Registry == "" && m.Reference == "" {
   237  		return span.Error(errors.New("a registry or reference value must be provided"))
   238  	}
   239  
   240  	if m.Reference != "" && m.Registry != "" {
   241  		span.Warnf("WARNING: both registry and reference were provided; "+
   242  			"using the reference value of %s for the bundle reference\n", m.Reference)
   243  	}
   244  
   245  	// Allow for the user to have specified the version with a leading v prefix but save it as
   246  	// proper semver
   247  	if m.Version != "" {
   248  		v, err := semver.NewVersion(m.Version)
   249  		if err != nil {
   250  			return span.Errorf("version %q is not a valid semver value: %w", m.Version, err)
   251  		}
   252  		m.Version = v.String()
   253  	}
   254  	return nil
   255  }
   256  
   257  var templatedOutputRegex = regexp.MustCompile(`^bundle\.outputs\.(.+)$`)
   258  
   259  // getTemplateOutputName returns the output name from the template variable.
   260  func (m *Manifest) getTemplateOutputName(value string) (string, bool) {
   261  	matches := templatedOutputRegex.FindStringSubmatch(value)
   262  	if len(matches) < 2 {
   263  		return "", false
   264  	}
   265  
   266  	outputName := matches[1]
   267  	return outputName, true
   268  }
   269  
   270  var templatedDependencyOutputRegex = regexp.MustCompile(`^bundle\.dependencies\.(.+).outputs.(.+)$`)
   271  
   272  // getTemplateDependencyOutputName returns the dependency and output name from the
   273  // template variable.
   274  func (m *Manifest) getTemplateDependencyOutputName(value string) (string, string, bool) {
   275  	matches := templatedDependencyOutputRegex.FindStringSubmatch(value)
   276  	if len(matches) < 3 {
   277  		return "", "", false
   278  	}
   279  
   280  	dependencyName := matches[1]
   281  	outputName := matches[2]
   282  	return dependencyName, outputName, true
   283  }
   284  
   285  var templatedDependencyShortOutputRegex = regexp.MustCompile(`^outputs.(.+)$`)
   286  
   287  // getTemplateDependencyShortOutputName returns the dependency output name from the
   288  // template variable.
   289  func (m *Manifest) getTemplateDependencyShortOutputName(value string) (string, bool) {
   290  	matches := templatedDependencyShortOutputRegex.FindStringSubmatch(value)
   291  	if len(matches) < 2 {
   292  		return "", false
   293  	}
   294  
   295  	outputName := matches[1]
   296  	return outputName, true
   297  }
   298  
   299  var templatedParameterRegex = regexp.MustCompile(`^bundle\.parameters\.(.+)$`)
   300  
   301  // GetTemplateParameterName returns the parameter name from the template variable.
   302  func (m *Manifest) GetTemplateParameterName(value string) (string, bool) {
   303  	matches := templatedParameterRegex.FindStringSubmatch(value)
   304  	if len(matches) < 2 {
   305  		return "", false
   306  	}
   307  
   308  	parameterName := matches[1]
   309  	return parameterName, true
   310  }
   311  
   312  // GetTemplatedOutputs returns the output definitions for any bundle level outputs
   313  // that have been templated, keyed by the output name.
   314  func (m *Manifest) GetTemplatedOutputs() OutputDefinitions {
   315  	outputs := make(OutputDefinitions, len(m.TemplateVariables))
   316  	for _, tmplVar := range m.TemplateVariables {
   317  		if name, ok := m.getTemplateOutputName(tmplVar); ok {
   318  			outputDef, ok := m.Outputs[name]
   319  			if !ok {
   320  				// Only return bundle level definitions
   321  				continue
   322  			}
   323  			outputs[name] = outputDef
   324  		}
   325  	}
   326  	return outputs
   327  }
   328  
   329  // GetTemplatedOutputs returns the output definitions for any bundle level outputs
   330  // that have been templated, keyed by "DEPENDENCY.OUTPUT".
   331  func (m *Manifest) GetTemplatedDependencyOutputs() DependencyOutputReferences {
   332  	outputs := make(DependencyOutputReferences, len(m.TemplateVariables))
   333  	for _, tmplVar := range m.TemplateVariables {
   334  		if dep, output, ok := m.getTemplateDependencyOutputName(tmplVar); ok {
   335  			ref := DependencyOutputReference{
   336  				Dependency: dep,
   337  				Output:     output,
   338  			}
   339  			outputs[ref.String()] = ref
   340  		}
   341  	}
   342  	return outputs
   343  }
   344  
   345  // GetTemplatedParameters returns the output definitions for any bundle level outputs
   346  // that have been templated, keyed by the output name.
   347  func (m *Manifest) GetTemplatedParameters() ParameterDefinitions {
   348  	parameters := make(ParameterDefinitions, len(m.TemplateVariables))
   349  	for _, tmplVar := range m.TemplateVariables {
   350  		if name, ok := m.GetTemplateParameterName(tmplVar); ok {
   351  			parameterDef, ok := m.Parameters[name]
   352  			if !ok {
   353  				// Only return bundle level definitions
   354  				continue
   355  			}
   356  			parameters[name] = parameterDef
   357  		}
   358  	}
   359  	return parameters
   360  }
   361  
   362  // DetermineDependenciesExtensionUsed looks for how dependencies are used
   363  // by the bundle and which version of the dependency extension can be used.
   364  func (m *Manifest) DetermineDependenciesExtensionUsed() string {
   365  	// Check if v2 deps are explicitly specified
   366  	for _, ext := range m.Required {
   367  		if ext.Name == cnab.DependenciesV2ExtensionShortHand ||
   368  			ext.Name == cnab.DependenciesV2ExtensionKey {
   369  			return cnab.DependenciesV2ExtensionKey
   370  		}
   371  	}
   372  
   373  	// Check each dependency for use of v2 only features
   374  	for _, dep := range m.Dependencies.Requires {
   375  		if dep.UsesV2Features() {
   376  			return cnab.DependenciesV2ExtensionKey
   377  		}
   378  	}
   379  
   380  	// Check if the bundle declares that it can satisfy a v2 dependency
   381  	if m.Dependencies.Provides != nil {
   382  		return cnab.DependenciesV2ExtensionKey
   383  	}
   384  
   385  	if len(m.Dependencies.Requires) > 0 {
   386  		// Dependencies are declared but only use v1 features
   387  		return cnab.DependenciesV1ExtensionKey
   388  	}
   389  
   390  	// No dependencies are used at all
   391  	return ""
   392  }
   393  
   394  type CustomDefinitions map[string]interface{}
   395  
   396  func (cd *CustomDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error {
   397  	raw, err := yaml.UnmarshalMap(unmarshal)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	*cd = raw
   402  	return nil
   403  }
   404  
   405  type DependencyOutputReference struct {
   406  	Dependency string
   407  	Output     string
   408  }
   409  
   410  func (r DependencyOutputReference) String() string {
   411  	return fmt.Sprintf("%s.%s", r.Dependency, r.Output)
   412  }
   413  
   414  type DependencyOutputReferences map[string]DependencyOutputReference
   415  
   416  // DependencyProvider specifies how the current bundle can be used to satisfy a dependency.
   417  type DependencyProvider struct {
   418  	// Interface declares the bundle interface that the current bundle provides.
   419  	Interface InterfaceDeclaration `yaml:"interface,omitempty"`
   420  }
   421  
   422  // InterfaceDeclaration declares that the current bundle supports the specified bundle interface
   423  // Reserved for future use. Right now we only use an interface id, but could support other fields later.
   424  type InterfaceDeclaration struct {
   425  	// ID is the URI of the interface that this bundle provides. Usually a well-known name defined by Porter or CNAB.
   426  	ID string `yaml:"id,omitempty"`
   427  }
   428  
   429  // ParameterDefinitions allows us to represent parameters as a list in the YAML
   430  // and work with them as a map internally
   431  type ParameterDefinitions map[string]ParameterDefinition
   432  
   433  func (pd ParameterDefinitions) MarshalYAML() (interface{}, error) {
   434  	raw := make([]ParameterDefinition, 0, len(pd))
   435  
   436  	for _, param := range pd {
   437  		raw = append(raw, param)
   438  	}
   439  
   440  	return raw, nil
   441  }
   442  
   443  func (pd *ParameterDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error {
   444  	var raw []ParameterDefinition
   445  	err := unmarshal(&raw)
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	if *pd == nil {
   451  		*pd = make(map[string]ParameterDefinition, len(raw))
   452  	}
   453  
   454  	for _, item := range raw {
   455  		(*pd)[item.Name] = item
   456  	}
   457  
   458  	return nil
   459  }
   460  
   461  var _ bundle.Scoped = &ParameterDefinition{}
   462  
   463  // ParameterDefinition defines a single parameter for a CNAB bundle
   464  type ParameterDefinition struct {
   465  	Name      string          `yaml:"name"`
   466  	Sensitive bool            `yaml:"sensitive"`
   467  	Source    ParameterSource `yaml:"source,omitempty"`
   468  
   469  	// These fields represent a subset of bundle.Parameter as defined in cnabio/cnab-go,
   470  	// minus the 'Description' field (definition.Schema's will be used) and `Definition` field
   471  	ApplyTo     []string `yaml:"applyTo,omitempty"`
   472  	Destination Location `yaml:",inline,omitempty"`
   473  
   474  	definition.Schema `yaml:",inline"`
   475  
   476  	// IsState identifies if the parameter was generated from a state variable
   477  	IsState bool `yaml:"-"`
   478  }
   479  
   480  func (pd *ParameterDefinition) GetApplyTo() []string {
   481  	return pd.ApplyTo
   482  }
   483  
   484  func (pd *ParameterDefinition) Validate() error {
   485  	var result *multierror.Error
   486  
   487  	if pd.Name == "" {
   488  		result = multierror.Append(result, errors.New("parameter name is required"))
   489  	}
   490  
   491  	// Porter supports declaring a parameter of type: "file",
   492  	// which we will convert to the appropriate bundle.Parameter type in adapter.go
   493  	// Here, we copy the ParameterDefinition and make the same modification before validation
   494  	pdCopy := pd.DeepCopy()
   495  	if pdCopy.Type == "file" {
   496  		if pd.Destination.Path == "" {
   497  			result = multierror.Append(result, fmt.Errorf("no destination path supplied for parameter %s", pd.Name))
   498  		}
   499  		pdCopy.Type = "string"
   500  		pdCopy.ContentEncoding = "base64"
   501  	}
   502  
   503  	// Validate the Parameter Definition schema itself
   504  	if _, err := pdCopy.Schema.ValidateSchema(); err != nil {
   505  		return multierror.Append(result, fmt.Errorf("encountered an error while validating definition for parameter %q: %w", pdCopy.Name, err))
   506  	}
   507  
   508  	if pdCopy.Default != nil {
   509  		schemaValidationErrs, err := pdCopy.Schema.Validate(pdCopy.Default)
   510  		if err != nil {
   511  			result = multierror.Append(result, fmt.Errorf("encountered error while validating parameter %s: %w", pdCopy.Name, err))
   512  		}
   513  		for _, schemaValidationErr := range schemaValidationErrs {
   514  			result = multierror.Append(result, fmt.Errorf("encountered an error validating the default value %v for parameter %q: %s", pdCopy.Default, pdCopy.Name, schemaValidationErr.Error))
   515  		}
   516  	}
   517  
   518  	return result.ErrorOrNil()
   519  }
   520  
   521  // DeepCopy copies a ParameterDefinition and returns the copy
   522  func (pd *ParameterDefinition) DeepCopy() *ParameterDefinition {
   523  	p2 := *pd
   524  	p2.ApplyTo = make([]string, len(pd.ApplyTo))
   525  	copy(p2.ApplyTo, pd.ApplyTo)
   526  	return &p2
   527  }
   528  
   529  // AppliesTo returns a boolean value specifying whether or not
   530  // the Parameter applies to the provided action
   531  func (pd *ParameterDefinition) AppliesTo(action string) bool {
   532  	return bundle.AppliesTo(pd, action)
   533  }
   534  
   535  // exemptFromInstall returns true if a parameter definition:
   536  //   - has an output source (which will not exist prior to install)
   537  //   - doesn't already have applyTo specified
   538  //   - doesn't have a default value
   539  func (pd *ParameterDefinition) exemptFromInstall() bool {
   540  	return pd.Source.Output != "" && pd.ApplyTo == nil && pd.Default == nil
   541  }
   542  
   543  // UpdateApplyTo updates a parameter definition's applyTo section
   544  // based on the provided manifest
   545  func (pd *ParameterDefinition) UpdateApplyTo(m *Manifest) {
   546  	if pd.exemptFromInstall() {
   547  		applyTo := []string{cnab.ActionUninstall}
   548  		// The core action "Upgrade" is technically still optional
   549  		// so only add it if it is declared in the manifest
   550  		if m.Upgrade != nil {
   551  			applyTo = append(applyTo, cnab.ActionUpgrade)
   552  		}
   553  		// Add all custom actions
   554  		for action := range m.CustomActions {
   555  			applyTo = append(applyTo, action)
   556  		}
   557  		sort.Strings(applyTo)
   558  		pd.ApplyTo = applyTo
   559  	}
   560  }
   561  
   562  type ParameterSource struct {
   563  	Dependency string `yaml:"dependency,omitempty"`
   564  	Output     string `yaml:"output"`
   565  }
   566  
   567  // CredentialDefinitions allows us to represent credentials as a list in the YAML
   568  // and work with them as a map internally
   569  type CredentialDefinitions map[string]CredentialDefinition
   570  
   571  func (cd CredentialDefinitions) MarshalYAML() (interface{}, error) {
   572  	raw := make([]CredentialDefinition, 0, len(cd))
   573  
   574  	for _, cred := range cd {
   575  		raw = append(raw, cred)
   576  	}
   577  
   578  	return raw, nil
   579  }
   580  
   581  func (cd *CredentialDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error {
   582  	var raw []CredentialDefinition
   583  	err := unmarshal(&raw)
   584  	if err != nil {
   585  		return err
   586  	}
   587  
   588  	if *cd == nil {
   589  		*cd = make(map[string]CredentialDefinition, len(raw))
   590  	}
   591  
   592  	for _, item := range raw {
   593  		(*cd)[item.Name] = item
   594  	}
   595  
   596  	return nil
   597  }
   598  
   599  // CredentialDefinition represents the structure or fields of a credential parameter
   600  type CredentialDefinition struct {
   601  	Name        string `yaml:"name"`
   602  	Description string `yaml:"description,omitempty"`
   603  
   604  	// Required specifies if the credential must be specified for applicable actions. Defaults to true.
   605  	Required bool `yaml:"required,omitempty"`
   606  
   607  	// ApplyTo lists the actions to which the credential applies. When unset, defaults to all actions.
   608  	ApplyTo []string `yaml:"applyTo,omitempty"`
   609  
   610  	Location `yaml:",inline"`
   611  }
   612  
   613  func (cd *CredentialDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error {
   614  	type rawCreds CredentialDefinition
   615  	rawCred := rawCreds{
   616  		Name:        cd.Name,
   617  		Description: cd.Description,
   618  		Required:    true,
   619  		Location:    cd.Location,
   620  	}
   621  
   622  	if err := unmarshal(&rawCred); err != nil {
   623  		return err
   624  	}
   625  
   626  	*cd = CredentialDefinition(rawCred)
   627  
   628  	return nil
   629  }
   630  
   631  // Location represents a Parameter or Credential location in an InvocationImage
   632  type Location struct {
   633  	Path                string `yaml:"path,omitempty"`
   634  	EnvironmentVariable string `yaml:"env,omitempty"`
   635  }
   636  
   637  func (l Location) IsEmpty() bool {
   638  	var empty Location
   639  	return l == empty
   640  }
   641  
   642  type MixinDeclaration struct {
   643  	Name    string
   644  	Version *semver.Constraints
   645  	Config  interface{}
   646  }
   647  
   648  func extractVersionFromName(name string) (string, *semver.Constraints, error) {
   649  	parts := strings.Split(name, "@")
   650  
   651  	// if there isn't a version in the name, just stop!
   652  	if len(parts) == 1 {
   653  		return name, nil, nil
   654  	}
   655  
   656  	// if we somehow got more parts than expected!
   657  	if len(parts) != 2 {
   658  		return "", nil, fmt.Errorf("expected name@version, got: %s", name)
   659  	}
   660  
   661  	version, err := semver.NewConstraint(parts[1])
   662  	if err != nil {
   663  		return "", nil, err
   664  	}
   665  
   666  	return parts[0], version, nil
   667  }
   668  
   669  // UnmarshalYAML allows mixin declarations to either be a normal list of strings
   670  // mixins:
   671  // - exec
   672  // - helm3
   673  // or allow some entries to have config data defined
   674  //   - az:
   675  //     extensions:
   676  //   - iot
   677  //
   678  // for each type, we can optionally support a version number in the name field
   679  // mixins:
   680  // - exec@2.1.1
   681  // or
   682  //   - az@2.1.1
   683  //     extensions:
   684  //   - iot
   685  func (m *MixinDeclaration) UnmarshalYAML(unmarshal func(interface{}) error) error {
   686  	// First try to just read the mixin name
   687  	var mixinNameOnly string
   688  	err := unmarshal(&mixinNameOnly)
   689  	if err == nil {
   690  		name, version, err := extractVersionFromName(mixinNameOnly)
   691  		if err != nil {
   692  			return fmt.Errorf("invalid mixin name/version: %w", err)
   693  		}
   694  		m.Name = name
   695  		m.Version = version
   696  		m.Config = nil
   697  		return nil
   698  	}
   699  
   700  	// Next try to read a mixin name with config defined
   701  	mixinWithConfig := map[string]interface{}{}
   702  	err = unmarshal(&mixinWithConfig)
   703  	if err != nil {
   704  		return fmt.Errorf("could not unmarshal raw yaml of mixin declarations: %w", err)
   705  	}
   706  
   707  	if len(mixinWithConfig) == 0 {
   708  		return errors.New("mixin declaration was empty")
   709  	} else if len(mixinWithConfig) > 1 {
   710  		return errors.New("mixin declaration contained more than one mixin")
   711  	}
   712  
   713  	for mixinName, config := range mixinWithConfig {
   714  		name, version, err := extractVersionFromName(mixinName)
   715  		if err != nil {
   716  			return fmt.Errorf("invalid mixin name/version: %w", err)
   717  		}
   718  		m.Name = name
   719  		m.Version = version
   720  		m.Config = config
   721  		break // There is only one mixin anyway but break for clarity
   722  	}
   723  	return nil
   724  }
   725  
   726  // MarshalYAML allows mixin declarations to either be a normal list of strings
   727  // mixins:
   728  // - exec
   729  // - helm3
   730  // or allow some entries to have config data defined
   731  //   - az:
   732  //     extensions:
   733  //   - iot
   734  func (m MixinDeclaration) MarshalYAML() (interface{}, error) {
   735  	if m.Config == nil {
   736  		return m.Name, nil
   737  	}
   738  
   739  	raw := map[string]interface{}{
   740  		m.Name: m.Config,
   741  	}
   742  	return raw, nil
   743  }
   744  
   745  type MappedImage struct {
   746  	Description string            `yaml:"description"`
   747  	ImageType   string            `yaml:"imageType"`
   748  	Repository  string            `yaml:"repository"`
   749  	Digest      string            `yaml:"digest,omitempty"`
   750  	Size        uint64            `yaml:"size,omitempty"`
   751  	MediaType   string            `yaml:"mediaType,omitempty"`
   752  	Labels      map[string]string `yaml:"labels,omitempty"`
   753  	Tag         string            `yaml:"tag,omitempty"`
   754  }
   755  
   756  func (mi *MappedImage) Validate() error {
   757  	if mi.Digest != "" {
   758  		if _, err := digest.Parse(mi.Digest); err != nil {
   759  			return err
   760  		}
   761  	}
   762  
   763  	if _, err := cnab.ParseOCIReference(mi.Repository); err != nil {
   764  		return err
   765  	}
   766  
   767  	return nil
   768  }
   769  
   770  func (mi *MappedImage) ToOCIReference() (cnab.OCIReference, error) {
   771  	ref, err := cnab.ParseOCIReference(mi.Repository)
   772  	if err != nil {
   773  		return cnab.OCIReference{}, err
   774  	}
   775  
   776  	if mi.Digest != "" {
   777  		refWithDigest, err := ref.WithDigest(digest.Digest(mi.Digest))
   778  		if err != nil {
   779  			return cnab.OCIReference{}, fmt.Errorf("failed to create a new reference with digest for repository %s: %w", mi.Repository, err)
   780  		}
   781  
   782  		return refWithDigest, nil
   783  	}
   784  
   785  	if mi.Tag != "" {
   786  		refWithTag, err := ref.WithTag(mi.Tag)
   787  		if err != nil {
   788  			return cnab.OCIReference{}, fmt.Errorf("failed to create a new reference with tag for repository %s: %w", mi.Repository, err)
   789  		}
   790  
   791  		return refWithTag, nil
   792  	}
   793  
   794  	return ref, nil
   795  }
   796  
   797  // Dependencies defies both v2 and v1 dependencies.
   798  // Dependencies v1 is a subset of Dependencies v2.
   799  type Dependencies struct {
   800  	// Requires specifies bundles required by the current bundle.
   801  	Requires []*Dependency `yaml:"requires,omitempty"`
   802  
   803  	// Provides specifies how the bundle can satisfy a dependency.
   804  	// This declares that the bundle can provide a dependency that another bundle requires.
   805  	Provides *DependencyProvider `yaml:"provides,omitempty"`
   806  }
   807  
   808  // Dependency defines a parent child relationship between this bundle (parent) and the specified bundle (child).
   809  type Dependency struct {
   810  	// Name of the dependency, used to reference the dependency from other parts of
   811  	// the bundle such as the template syntax, bundle.dependencies.NAME
   812  	Name string `yaml:"name"`
   813  
   814  	// Bundle specifies criteria for selecting a bundle to satisfy the dependency.
   815  	Bundle BundleCriteria `yaml:"bundle"`
   816  
   817  	// Sharing is a set of rules for sharing a dependency with other bundles.
   818  	Sharing SharingCriteria `yaml:"sharing,omitempty"`
   819  
   820  	// Parameters is a map of values, keyed by the destination where the value is the
   821  	// source, to pass from the bundle to the dependency. May either be a hard-coded
   822  	// value, or a template value such as ${bundle.parameters.NAME}. The key is the
   823  	// dependency's parameter name, and the value is the data being passed to the
   824  	// dependency parameter.
   825  	Parameters map[string]string `yaml:"parameters,omitempty"`
   826  
   827  	// Credentials is a map of values, keyed by the destination where the value is
   828  	// the source, to pass from the bundle to the dependency. May either be a
   829  	// hard-coded value, or a template value such as ${bundle.credentials.NAME}. The
   830  	// key is the dependency's credential name, and the value is the data being
   831  	// passed to the dependency credential.
   832  	Credentials map[string]string `yaml:"credentials,omitempty"`
   833  
   834  	// Outputs is a map of values, keyed by the destination where the value is the
   835  	// source, to pass from the dependency and promote to a bundle-level outputs of
   836  	// the parent bundle. May either be the name of an output from the dependency, or
   837  	// a template value such as ${outputs.NAME} where the outputs variable holds the
   838  	// current dependency's outputs. The long form of the template syntax,
   839  	// ${bundle.dependencies.DEP.outputs.NAME}, is also supported. The key is the
   840  	// parent bundle's output name, and the value is the data being passed to the
   841  	// dependency parameter.
   842  	Outputs map[string]string `yaml:"outputs,omitempty"`
   843  }
   844  
   845  type DependencySource string
   846  
   847  // BundleCriteria criteria for selecting a bundle to satisfy a dependency.
   848  type BundleCriteria struct {
   849  	// Reference is an OCI reference to a bundle for use as the default implementation of the bundle.
   850  	// It should be in the format REGISTRY/NAME:TAG
   851  	Reference string `yaml:"reference"`
   852  
   853  	// "When constraint checking is used for checks or validation
   854  	// it will follow a different set of rules that are common for ranges with tools like npm/js and Rust/Cargo.
   855  	// This includes considering prereleases to be invalid if the ranges does not include one.
   856  	// If you want to have it include pre-releases a simple solution is to include -0 in your range."
   857  	// https://github.com/Masterminds/semver/blob/master/README.md#checking-version-constraints
   858  	Version string `yaml:"version,omitempty"`
   859  
   860  	// Interface specifies criteria for allowing a bundle to satisfy a dependency.
   861  	Interface *BundleInterface `yaml:"interface,omitempty"`
   862  }
   863  
   864  // BundleInterface specifies how a bundle can satisfy a dependency.
   865  // Porter always infers a base interface based on how the dependency is used in porter.yaml
   866  // but this allows the bundle author to extend it and add additional restrictions.
   867  // Either bundle or reference may be specified but not both.
   868  type BundleInterface struct {
   869  	// ID is the identifier or name of the bundle interface. It should be matched
   870  	// against the Dependencies.Provides.Interface.ID to determine if two interfaces
   871  	// are equivalent.
   872  	ID string `yaml:"id,omitempty"`
   873  
   874  	// Reference specifies an OCI reference to a bundle to use as the interface on top of how the bundle is used.
   875  	Reference string `yaml:"reference,omitempty"`
   876  
   877  	// Document specifies additional constraints that should be added to the bundle interface.
   878  	// By default, Porter only requires the name and the type to match, additional jsonschema values can be specified to restrict matching bundles even further.
   879  	// The value should be a jsonschema document containing relevant sub-documents from a bundle.json that should be applied to the base bundle interface.
   880  	Document *BundleInterfaceDocument `yaml:"document,omitempty"`
   881  }
   882  
   883  // BundleInterfaceDocument specifies the interface that a bundle must support in
   884  // order to satisfy a dependency.
   885  type BundleInterfaceDocument struct {
   886  	// Parameters that are defined on the interface.
   887  	Parameters ParameterDefinitions `yaml:"parameters,omitempty"`
   888  
   889  	// Credentials that are defined on the interface.
   890  	Credentials CredentialDefinitions `yaml:"credentials,omitempty"`
   891  
   892  	// Outputs that are defined on the interface.
   893  	Outputs OutputDefinitions `yaml:"outputs,omitempty"`
   894  }
   895  
   896  // SharingCriteria is a set of rules for sharing a dependency with other bundles.
   897  type SharingCriteria struct {
   898  	// Mode defines how a dependency can be shared.
   899  	//  - false: The dependency cannot be shared, even within the same dependency graph.
   900  	//  - true: The dependency is shared with other bundles who defined the dependency
   901  	//    with the same sharing group. This is the default mode.
   902  	Mode bool `yaml:"mode"`
   903  
   904  	// Group defines matching criteria for determining if two dependencies are in the same sharing group.
   905  	Group SharingGroup `yaml:"group,omitempty"`
   906  }
   907  
   908  // GetEffectiveMode returns the mode, taking into account the default value when
   909  // no mode is specified.
   910  func (s SharingCriteria) GetEffectiveMode() bool {
   911  	return s.Mode
   912  }
   913  
   914  // SharingGroup defines a set of characteristics for sharing a dependency with
   915  // other bundles.
   916  // Reserved for future use: We can add more characteristics later to expands how we share if needed
   917  type SharingGroup struct {
   918  	// Name of the sharing group. The name of the group must match for two bundles to share the same dependency.
   919  	Name string `yaml:"name"`
   920  }
   921  
   922  func (d *Dependency) Validate(cxt *portercontext.Context) error {
   923  	if d.Name == "" {
   924  		return errors.New("dependency name is required")
   925  	}
   926  
   927  	if d.Bundle.Reference == "" {
   928  		return fmt.Errorf("reference is required for dependency %q", d.Name)
   929  	}
   930  
   931  	ref, err := cnab.ParseOCIReference(d.Bundle.Reference)
   932  	if err != nil {
   933  		return fmt.Errorf("invalid reference %s for dependency %s: %w", d.Bundle.Reference, d.Name, err)
   934  	}
   935  
   936  	if ref.IsRepositoryOnly() && d.Bundle.Version == "" {
   937  		return fmt.Errorf("reference for dependency %q can specify only a repository, without a digest or tag, when a version constraint is specified", d.Name)
   938  	}
   939  
   940  	return nil
   941  }
   942  
   943  // UsesV2Features returns true if the dependency uses features from v2 of
   944  // Porter's implementation of dependencies, and returns false if the v1
   945  // implementation of dependencies would suffice.
   946  func (d *Dependency) UsesV2Features() bool {
   947  	// Sharing was added in v2
   948  	if d.Sharing != (SharingCriteria{}) {
   949  		return true
   950  	}
   951  
   952  	// Credentials and output mapping was added in v2
   953  	if len(d.Credentials) > 0 || len(d.Outputs) > 0 {
   954  		return true
   955  	}
   956  
   957  	// Bundle interfaces was added in v2
   958  	if d.Bundle.Interface != nil {
   959  		return true
   960  	}
   961  
   962  	// Anything else can be handled with v1
   963  	return false
   964  }
   965  
   966  type CustomActionDefinition struct {
   967  	Description       string `yaml:"description,omitempty"`
   968  	ModifiesResources bool   `yaml:"modifies,omitempty"`
   969  	Stateless         bool   `yaml:"stateless,omitempty"`
   970  }
   971  
   972  // OutputDefinitions allows us to represent parameters as a list in the YAML
   973  // and work with them as a map internally
   974  type OutputDefinitions map[string]OutputDefinition
   975  
   976  func (od OutputDefinitions) MarshalYAML() (interface{}, error) {
   977  	raw := make([]OutputDefinition, 0, len(od))
   978  
   979  	for _, output := range od {
   980  		raw = append(raw, output)
   981  	}
   982  
   983  	return raw, nil
   984  }
   985  
   986  func (od *OutputDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error {
   987  	var raw []OutputDefinition
   988  	err := unmarshal(&raw)
   989  	if err != nil {
   990  		return err
   991  	}
   992  
   993  	if *od == nil {
   994  		*od = make(map[string]OutputDefinition, len(raw))
   995  	}
   996  
   997  	for _, item := range raw {
   998  		(*od)[item.Name] = item
   999  	}
  1000  
  1001  	return nil
  1002  }
  1003  
  1004  // OutputDefinition defines a single output for a CNAB
  1005  type OutputDefinition struct {
  1006  	Name      string   `yaml:"name"`
  1007  	ApplyTo   []string `yaml:"applyTo,omitempty"`
  1008  	Sensitive bool     `yaml:"sensitive"`
  1009  
  1010  	// This is not in the CNAB spec, but it allows a mixin to create a file
  1011  	// and porter will take care of making it a proper output.
  1012  	Path string `yaml:"path,omitempty"`
  1013  
  1014  	definition.Schema `yaml:",inline"`
  1015  
  1016  	// IsState identifies if the output was generated from a state variable
  1017  	IsState bool `yaml:"-"`
  1018  }
  1019  
  1020  // DeepCopy copies a ParameterDefinition and returns the copy
  1021  func (od *OutputDefinition) DeepCopy() *OutputDefinition {
  1022  	o2 := *od
  1023  	o2.ApplyTo = make([]string, len(od.ApplyTo))
  1024  	copy(o2.ApplyTo, od.ApplyTo)
  1025  	return &o2
  1026  }
  1027  
  1028  func (od *OutputDefinition) Validate() error {
  1029  	var result *multierror.Error
  1030  
  1031  	if od.Name == "" {
  1032  		return errors.New("output name is required")
  1033  	}
  1034  
  1035  	// Porter supports declaring an output of type: "file",
  1036  	// which we will convert to the appropriate type in adapter.go
  1037  	// Here, we copy the definition and make the same modification before validation
  1038  	odCopy := od.DeepCopy()
  1039  	if odCopy.Type == "file" {
  1040  		if od.Path == "" {
  1041  			result = multierror.Append(result, fmt.Errorf("no path supplied for output %s", od.Name))
  1042  		}
  1043  		odCopy.Type = "string"
  1044  		odCopy.ContentEncoding = "base64"
  1045  	}
  1046  
  1047  	// Validate the Output Definition schema itself
  1048  	if _, err := odCopy.Schema.ValidateSchema(); err != nil {
  1049  		return multierror.Append(result, fmt.Errorf("encountered an error while validating definition for output %q: %w", odCopy.Name, err))
  1050  	}
  1051  
  1052  	if odCopy.Default != nil {
  1053  		schemaValidationErrs, err := odCopy.Schema.Validate(odCopy.Default)
  1054  		if err != nil {
  1055  			result = multierror.Append(result, fmt.Errorf("encountered error while validating output %s: %w", odCopy.Name, err))
  1056  		}
  1057  		for _, schemaValidationErr := range schemaValidationErrs {
  1058  			result = multierror.Append(result, fmt.Errorf("encountered an error validating the default value %v for output %q: %s", odCopy.Default, odCopy.Name, schemaValidationErr.Error))
  1059  		}
  1060  	}
  1061  
  1062  	return result.ErrorOrNil()
  1063  }
  1064  
  1065  type BundleOutput struct {
  1066  	Name                string `yaml:"name"`
  1067  	Path                string `yaml:"path"`
  1068  	EnvironmentVariable string `yaml:"env"`
  1069  }
  1070  
  1071  type Steps []*Step
  1072  
  1073  func (s Steps) Validate(m *Manifest) error {
  1074  	for i, step := range s {
  1075  		err := step.Validate(m)
  1076  		if err != nil {
  1077  			return fmt.Errorf("failed to validate %s step: %s", humanize.Ordinal(i+1), err)
  1078  		}
  1079  	}
  1080  	return nil
  1081  }
  1082  
  1083  type Step struct {
  1084  	Data map[string]interface{} `yaml:",inline"`
  1085  }
  1086  
  1087  func (s *Step) Validate(m *Manifest) error {
  1088  	if s == nil {
  1089  		return errors.New("found an empty step")
  1090  	}
  1091  	if len(s.Data) == 0 {
  1092  		return errors.New("no mixin specified")
  1093  	}
  1094  	if len(s.Data) > 1 {
  1095  		return errors.New("malformed step, possibly incorrect indentation")
  1096  	}
  1097  
  1098  	mixinDeclared := false
  1099  	mixinType := s.GetMixinName()
  1100  	for _, mixin := range m.Mixins {
  1101  		if mixin.Name == mixinType {
  1102  			mixinDeclared = true
  1103  			break
  1104  		}
  1105  	}
  1106  	if !mixinDeclared {
  1107  		return fmt.Errorf("mixin (%s) was not declared", mixinType)
  1108  	}
  1109  
  1110  	if _, err := s.GetDescription(); err != nil {
  1111  		return err
  1112  	}
  1113  
  1114  	return nil
  1115  }
  1116  
  1117  // GetDescription returns a description of the step.
  1118  // Every step must have this property.
  1119  func (s *Step) GetDescription() (string, error) {
  1120  	if s.Data == nil {
  1121  		return "", errors.New("empty step data")
  1122  	}
  1123  
  1124  	mixinName := s.GetMixinName()
  1125  	children := s.Data[mixinName]
  1126  	m, ok := children.(map[string]interface{})
  1127  	if !ok {
  1128  		return "", fmt.Errorf("invalid mixin type (%T) for mixin step (%s)", children, mixinName)
  1129  	}
  1130  	d := m["description"]
  1131  	if d == nil {
  1132  		return "", nil
  1133  	}
  1134  	desc, ok := d.(string)
  1135  	if !ok {
  1136  		return "", fmt.Errorf("invalid description type (%T) for mixin step (%s)", desc, mixinName)
  1137  	}
  1138  
  1139  	return desc, nil
  1140  }
  1141  
  1142  func (s *Step) GetMixinName() string {
  1143  	var mixinName string
  1144  	for k := range s.Data {
  1145  		mixinName = k
  1146  	}
  1147  	return mixinName
  1148  }
  1149  
  1150  func UnmarshalManifest(cxt *portercontext.Context, manifestData []byte) (*Manifest, error) {
  1151  	// Unmarshal the manifest into the normal struct
  1152  	manifest := &Manifest{}
  1153  	err := yaml.Unmarshal(manifestData, &manifest)
  1154  	if err != nil {
  1155  		return nil, fmt.Errorf("error unmarshaling the typed manifest: %w", err)
  1156  	}
  1157  
  1158  	// Do a second pass to identify custom actions, which don't have yaml tags since they are dynamic
  1159  	// 1. Marshal the manifest a second time into a plain map
  1160  	// 2. Remove keys for fields that are already mapped with yaml tags
  1161  	// 3. Anything left is a custom action
  1162  
  1163  	// Marshal the manifest into an untyped map
  1164  	unmappedData := make(map[string]interface{})
  1165  	err = yaml.Unmarshal(manifestData, &unmappedData)
  1166  	if err != nil {
  1167  		return nil, fmt.Errorf("error unmarshaling the untyped manifest: %w", err)
  1168  	}
  1169  
  1170  	// Use reflection to figure out which fields are on the manifest and have yaml tags
  1171  	objValue := reflect.ValueOf(manifest).Elem()
  1172  	knownFields := map[string]reflect.Value{}
  1173  	for i := 0; i != objValue.NumField(); i++ {
  1174  		tagName := strings.Split(objValue.Type().Field(i).Tag.Get("yaml"), ",")[0]
  1175  		knownFields[tagName] = objValue.Field(i)
  1176  	}
  1177  
  1178  	// Remove any fields that have yaml tags
  1179  	for key := range unmappedData {
  1180  		if _, found := knownFields[key]; found {
  1181  			delete(unmappedData, key)
  1182  		}
  1183  		// Delete known deprecated fields with no yaml tags
  1184  		if key == "invocationImage" || key == "tag" {
  1185  			fmt.Fprintf(cxt.Out, "WARNING: The %q field has been deprecated and can no longer be user-specified; ignoring.\n", key)
  1186  			delete(unmappedData, key)
  1187  		}
  1188  	}
  1189  
  1190  	// Marshal the remaining keys in the unmappedData as custom actions and append them to the typed manifest
  1191  	manifest.CustomActions = make(map[string]Steps, len(unmappedData))
  1192  	for key, chunk := range unmappedData {
  1193  		chunkData, err := yaml.Marshal(chunk)
  1194  		if err != nil {
  1195  			return nil, fmt.Errorf("error remarshaling custom action %s: %w", key, err)
  1196  		}
  1197  
  1198  		steps := Steps{}
  1199  		err = yaml.Unmarshal(chunkData, &steps)
  1200  		if err != nil {
  1201  			return nil, fmt.Errorf("error unmarshaling custom action %s: %w", key, err)
  1202  		}
  1203  
  1204  		manifest.CustomActions[key] = steps
  1205  	}
  1206  
  1207  	return manifest, nil
  1208  }
  1209  
  1210  // SetDefaults updates the manifest with default values where not populated
  1211  func (m *Manifest) SetDefaults() error {
  1212  	return m.SetBundleImageAndReference("")
  1213  }
  1214  
  1215  // SetBundleImageAndReference sets the bundle image name and the
  1216  // bundle reference on the manifest per the provided reference or via the
  1217  // registry or name values on the manifest.
  1218  func (m *Manifest) SetBundleImageAndReference(ref string) error {
  1219  	if ref != "" {
  1220  		m.Reference = ref
  1221  	}
  1222  
  1223  	if m.Reference == "" && m.Registry != "" {
  1224  		repo, err := cnab.ParseOCIReference(path.Join(m.Registry, m.Name))
  1225  		if err != nil {
  1226  			return fmt.Errorf("invalid bundle reference %s: %w", path.Join(m.Registry, m.Name), err)
  1227  		}
  1228  		m.Reference = repo.Repository()
  1229  	}
  1230  
  1231  	bundleRef, err := cnab.ParseOCIReference(m.Reference)
  1232  	if err != nil {
  1233  		return fmt.Errorf("invalid bundle reference %s: %w", m.Reference, err)
  1234  	}
  1235  
  1236  	dockerTag, err := m.getDockerTagFromBundleRef(bundleRef)
  1237  	if err != nil {
  1238  		return fmt.Errorf("unable to derive docker tag from bundle reference %q: %w", m.Reference, err)
  1239  	}
  1240  
  1241  	// If the docker tag is initially missing from bundleTag, update with
  1242  	// returned dockerTag
  1243  	if !bundleRef.HasTag() {
  1244  		bundleRef, err = bundleRef.WithTag(dockerTag)
  1245  		if err != nil {
  1246  			return fmt.Errorf("could not set bundle tag to %q: %w", dockerTag, err)
  1247  		}
  1248  		m.Reference = bundleRef.String()
  1249  	}
  1250  
  1251  	installerImage, err := cnab.CalculateTemporaryImageTag(bundleRef)
  1252  	if err != nil {
  1253  		return err
  1254  	}
  1255  
  1256  	m.Image = installerImage.String()
  1257  	return nil
  1258  }
  1259  
  1260  // getDockerTagFromBundleRef returns the Docker tag portion of the bundle tag,
  1261  // using the bundle version as a fallback
  1262  func (m *Manifest) getDockerTagFromBundleRef(bundleRef cnab.OCIReference) (string, error) {
  1263  	// If the manifest has a DockerTag override already set (e.g. on publish), use this
  1264  	if m.DockerTag != "" {
  1265  		return m.DockerTag, nil
  1266  	}
  1267  
  1268  	if bundleRef.HasTag() {
  1269  		return bundleRef.Tag(), nil
  1270  	}
  1271  
  1272  	if bundleRef.HasDigest() {
  1273  		return "", errors.New("invalid bundle tag format, must be an OCI image tag")
  1274  	}
  1275  
  1276  	// Docker tag is missing from the provided bundle tag, so default it
  1277  	// to use the manifest version prefixed with v
  1278  	// Example: bundle version is 1.0.0, so the bundle tag is v1.0.0
  1279  	newRef, err := bundleRef.WithVersion(m.Version)
  1280  	if err != nil {
  1281  		return "", err
  1282  	}
  1283  	return newRef.Tag(), nil
  1284  }
  1285  
  1286  // ResolvePath resolves a path specified in the Porter manifest into
  1287  // an absolute path, assuming the current directory is /cnab/app.
  1288  // Returns an empty string when the specified value is empty.
  1289  func ResolvePath(value string) string {
  1290  	if value == "" {
  1291  		return ""
  1292  	}
  1293  
  1294  	if path.IsAbs(value) {
  1295  		return value
  1296  	}
  1297  
  1298  	return path.Join("/cnab/app", value)
  1299  }
  1300  
  1301  func readFromFile(cxt *portercontext.Context, path string) ([]byte, error) {
  1302  	if exists, _ := cxt.FileSystem.Exists(path); !exists {
  1303  		return nil, fmt.Errorf("the specified porter configuration file %s does not exist", path)
  1304  	}
  1305  
  1306  	data, err := cxt.FileSystem.ReadFile(path)
  1307  	if err != nil {
  1308  		return nil, fmt.Errorf("could not read manifest at %q: %w", path, err)
  1309  	}
  1310  	return data, nil
  1311  }
  1312  
  1313  func readFromURL(path string) ([]byte, error) {
  1314  	resp, err := http.Get(path)
  1315  	if err != nil {
  1316  		return nil, fmt.Errorf("could not reach url %s: %w", path, err)
  1317  	}
  1318  
  1319  	defer resp.Body.Close()
  1320  	data, err := io.ReadAll(resp.Body)
  1321  	if err != nil {
  1322  		return nil, fmt.Errorf("could not read from url %s: %w", path, err)
  1323  	}
  1324  	return data, nil
  1325  }
  1326  
  1327  func ReadManifestData(cxt *portercontext.Context, path string) ([]byte, error) {
  1328  	if strings.HasPrefix(path, "http") {
  1329  		return readFromURL(path)
  1330  	} else {
  1331  		return readFromFile(cxt, path)
  1332  	}
  1333  }
  1334  
  1335  // ReadManifest determines if specified path is a URL or a filepath.
  1336  // After reading the data in the path it returns a Manifest and any errors
  1337  func ReadManifest(cxt *portercontext.Context, path string, config *config.Config) (*Manifest, error) {
  1338  	data, err := ReadManifestData(cxt, path)
  1339  	if err != nil {
  1340  		return nil, err
  1341  	}
  1342  
  1343  	m, err := UnmarshalManifest(cxt, data)
  1344  	if err != nil {
  1345  		return nil, fmt.Errorf("unsupported property set or a custom action is defined incorrectly: %w", err)
  1346  	}
  1347  
  1348  	tmplResult, err := m.ScanManifestTemplating(data, config)
  1349  	if err != nil {
  1350  		return nil, err
  1351  	}
  1352  
  1353  	m.ManifestPath = path
  1354  	m.TemplateVariables = tmplResult.Variables
  1355  
  1356  	return m, nil
  1357  }
  1358  
  1359  // templateScanResult is the result of parsing the mustache templating used in the manifest.
  1360  type templateScanResult struct {
  1361  	// Variables used in the template, e.g.  {{ bundle.parameters.NAME }}
  1362  	Variables []string
  1363  }
  1364  
  1365  func (m *Manifest) GetTemplatePrefix() string {
  1366  	if m.SchemaVersion == "" {
  1367  		// Super-old bundles use the mustache default
  1368  		return ""
  1369  	}
  1370  
  1371  	// In 1.0.0-alpha.2+, the prefix is ${}. Beforehand it was {{}}
  1372  	v, err := semver.NewVersion(m.SchemaVersion)
  1373  	if err == nil {
  1374  		if v.GreaterThan(semver.MustParse("v1.0.0-alpha.1")) {
  1375  			// Change the delimiter
  1376  			return TemplateDelimiterPrefix
  1377  		}
  1378  	}
  1379  
  1380  	// Fallback to the mustache default if we can't determine the schema version
  1381  	return ""
  1382  }
  1383  
  1384  func (m *Manifest) ScanManifestTemplating(data []byte, config *config.Config) (templateScanResult, error) {
  1385  	// Handle outputs variable
  1386  	shortOutputVars, err := m.mapShortDependencyOutputVariables(config)
  1387  	if err != nil {
  1388  		return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err)
  1389  	}
  1390  
  1391  	vars, err := m.getTemplateVariables(string(data))
  1392  	if err != nil {
  1393  		return templateScanResult{}, fmt.Errorf("error parsing the templating used in the manifest: %w", err)
  1394  	}
  1395  
  1396  	if config.IsFeatureEnabled(experimental.FlagDependenciesV2) {
  1397  		m.deduplicateAndFilterShortOutputVariables(shortOutputVars, vars)
  1398  	}
  1399  
  1400  	result := templateScanResult{
  1401  		Variables: make([]string, 0, len(vars)),
  1402  	}
  1403  	for v := range vars {
  1404  		result.Variables = append(result.Variables, v)
  1405  	}
  1406  
  1407  	sort.Strings(result.Variables)
  1408  	return result, nil
  1409  }
  1410  
  1411  func (m *Manifest) mapShortDependencyOutputVariables(config *config.Config) ([]string, error) {
  1412  	shortOutputVars := []string{}
  1413  	if config.IsFeatureEnabled(experimental.FlagDependenciesV2) {
  1414  		for _, dep := range m.Dependencies.Requires {
  1415  			for outputName, output := range dep.Outputs {
  1416  				vars, err := m.getTemplateVariables(output)
  1417  				if err != nil {
  1418  					return nil, fmt.Errorf("error parsing the templating used for dependency %s output %s: %w", dep.Name, outputName, err)
  1419  				}
  1420  
  1421  				for tmplVar := range vars {
  1422  					outputTemplateName, ok := m.getTemplateDependencyShortOutputName(tmplVar)
  1423  					if ok {
  1424  						shortOutputVars = append(shortOutputVars, fmt.Sprintf("bundle.dependencies.%s.outputs.%s", dep.Name, outputTemplateName))
  1425  					}
  1426  				}
  1427  			}
  1428  		}
  1429  	}
  1430  
  1431  	return shortOutputVars, nil
  1432  }
  1433  
  1434  func (m *Manifest) deduplicateAndFilterShortOutputVariables(shortOutputVars []string, vars map[string]struct{}) {
  1435  	for tmplVar := range vars {
  1436  		if strings.HasPrefix(tmplVar, "outputs.") {
  1437  			delete(vars, tmplVar)
  1438  		}
  1439  	}
  1440  
  1441  	for _, shortHandVar := range shortOutputVars {
  1442  		vars[shortHandVar] = struct{}{}
  1443  	}
  1444  }
  1445  
  1446  func (m *Manifest) getTemplateVariables(data string) (map[string]struct{}, error) {
  1447  	const disableHtmlEscaping = true
  1448  	templateSrc := m.GetTemplatePrefix() + string(data)
  1449  	tmpl, err := mustache.ParseStringRaw(templateSrc, disableHtmlEscaping)
  1450  	if err != nil {
  1451  		return nil, fmt.Errorf("error parsing the templating used in the manifest: %w", err)
  1452  	}
  1453  
  1454  	tags := tmpl.Tags()
  1455  	vars := map[string]struct{}{} // Keep track of unique variable names
  1456  	for _, tag := range tags {
  1457  		if tag.Type() != mustache.Variable {
  1458  			continue
  1459  		}
  1460  
  1461  		vars[tag.Name()] = struct{}{}
  1462  	}
  1463  
  1464  	return vars, nil
  1465  }
  1466  
  1467  // LoadManifestFrom reads and validates the manifest at the specified location,
  1468  // and returns a populated Manifest structure.
  1469  func LoadManifestFrom(ctx context.Context, config *config.Config, file string) (*Manifest, error) {
  1470  	ctx, log := tracing.StartSpan(ctx)
  1471  	defer log.EndSpan()
  1472  
  1473  	m, err := ReadManifest(config.Context, file, config)
  1474  	if err != nil {
  1475  		return nil, err
  1476  	}
  1477  
  1478  	if err = m.Validate(ctx, config); err != nil {
  1479  		return nil, err
  1480  	}
  1481  
  1482  	return m, nil
  1483  }
  1484  
  1485  // RequiredExtension represents a custom extension that is required
  1486  // in order for a bundle to work correctly
  1487  type RequiredExtension struct {
  1488  	Name   string
  1489  	Config map[string]interface{}
  1490  }
  1491  
  1492  // UnmarshalYAML allows required extensions to either be a normal list of strings
  1493  // required:
  1494  // - docker
  1495  // or allow some entries to have config data defined
  1496  //   - vpn:
  1497  //     name: mytrustednetwork
  1498  func (r *RequiredExtension) UnmarshalYAML(unmarshal func(interface{}) error) error {
  1499  	// First try to just read the mixin name
  1500  	var extNameOnly string
  1501  	err := unmarshal(&extNameOnly)
  1502  	if err == nil {
  1503  		r.Name = extNameOnly
  1504  		r.Config = nil
  1505  		return nil
  1506  	}
  1507  
  1508  	// Next try to read a required extension with config defined
  1509  	extWithConfig := map[string]map[string]interface{}{}
  1510  	err = unmarshal(&extWithConfig)
  1511  	if err != nil {
  1512  		return fmt.Errorf("could not unmarshal raw yaml of required extensions: %w", err)
  1513  	}
  1514  
  1515  	if len(extWithConfig) == 0 {
  1516  		return errors.New("required extension was empty")
  1517  	} else if len(extWithConfig) > 1 {
  1518  		return errors.New("required extension contained more than one extension")
  1519  	}
  1520  
  1521  	for extName, config := range extWithConfig {
  1522  		r.Name = extName
  1523  		r.Config = config
  1524  		break // There is only one extension anyway but break for clarity
  1525  	}
  1526  	return nil
  1527  }
  1528  
  1529  // Convert a parameter name to an environment variable.
  1530  // Anything more complicated should define the variable explicitly.
  1531  func ParamToEnvVar(name string) string {
  1532  	name = strings.ToUpper(name)
  1533  	fixer := strings.NewReplacer("-", "_", ".", "_")
  1534  	return fixer.Replace(name)
  1535  }
  1536  
  1537  // GetParameterSourceForOutput builds the parameter source name used by Porter
  1538  // internally for wiring up an output to a parameter.
  1539  func GetParameterSourceForOutput(outputName string) string {
  1540  	return fmt.Sprintf("porter-%s-output", outputName)
  1541  }
  1542  
  1543  // GetParameterSourceForDependency builds the parameter source name used by Porter
  1544  // internally for wiring up an dependency's output to a parameter.
  1545  func GetParameterSourceForDependency(ref DependencyOutputReference) string {
  1546  	return fmt.Sprintf("porter-%s-%s-dep-output", ref.Dependency, ref.Output)
  1547  }
  1548  
  1549  type MaintainerDefinition struct {
  1550  	Name  string `yaml:"name,omitempty"`
  1551  	Email string `yaml:"email,omitempty"`
  1552  	Url   string `yaml:"url,omitempty"`
  1553  }
  1554  
  1555  // StateBag is the set of state files and variables that Porter should
  1556  // track between bundle executions.
  1557  type StateBag []StateVariable
  1558  
  1559  type StateVariable struct {
  1560  	// Name of the state variable
  1561  	Name string `yaml:"name"`
  1562  
  1563  	// Description of the state variable and how it's used by the bundle
  1564  	Description string `yaml:"description,omitempty"`
  1565  
  1566  	// Mixin is the name of the mixin that manages the state variable.
  1567  	Mixin string `yaml:"mixin,omitempty"`
  1568  
  1569  	// Location defines where the state variable is located in the bundle.
  1570  	Location `yaml:",inline"`
  1571  }