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

     1  package runtime
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  	"reflect"
    15  	"strings"
    16  
    17  	"get.porter.sh/porter/pkg"
    18  	"get.porter.sh/porter/pkg/cnab"
    19  	"get.porter.sh/porter/pkg/config"
    20  	"get.porter.sh/porter/pkg/experimental"
    21  	"get.porter.sh/porter/pkg/manifest"
    22  	"get.porter.sh/porter/pkg/tracing"
    23  	"get.porter.sh/porter/pkg/yaml"
    24  	"github.com/cbroglie/mustache"
    25  	"github.com/cnabio/cnab-go/bundle"
    26  	"github.com/cnabio/cnab-to-oci/relocation"
    27  	"github.com/hashicorp/go-multierror"
    28  	"github.com/opencontainers/go-digest"
    29  	yaml3 "gopkg.in/yaml.v3"
    30  )
    31  
    32  const (
    33  	// Path to the archive of the state from the previous run
    34  	statePath = "/porter/state.tgz"
    35  )
    36  
    37  type RuntimeManifest struct {
    38  	config RuntimeConfig
    39  	*manifest.Manifest
    40  
    41  	Action string
    42  
    43  	// bundle is the executing bundle definition
    44  	bundle cnab.ExtendedBundle
    45  
    46  	// editor is the porter.yaml loaded into YQ so that we can
    47  	// do advanced stuff with the manifest, like just read out the yaml for a particular step.
    48  	editor *yaml.Editor
    49  
    50  	// bundles is map of the dependencies bundle definitions, keyed by the alias used in the root manifest
    51  	bundles map[string]cnab.ExtendedBundle
    52  
    53  	steps           manifest.Steps
    54  	outputs         map[string]string
    55  	sensitiveValues []string
    56  }
    57  
    58  func NewRuntimeManifest(cfg RuntimeConfig, action string, manifest *manifest.Manifest) *RuntimeManifest {
    59  	return &RuntimeManifest{
    60  		config:   cfg,
    61  		Action:   action,
    62  		Manifest: manifest,
    63  	}
    64  }
    65  
    66  // this is a temporary function to help write debug logs until we have PORTER_VERBOSITY passed into the bundle properly
    67  func (m *RuntimeManifest) debugf(log tracing.TraceLogger, msg string, args ...interface{}) {
    68  	if m.config.DebugMode {
    69  		log.Infof(msg, args...)
    70  	}
    71  }
    72  
    73  func (m *RuntimeManifest) Validate() error {
    74  	err := m.loadBundle()
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	err = m.loadDependencyDefinitions()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	err = m.setStepsByAction()
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	err = m.steps.Validate(m.Manifest)
    90  	if err != nil {
    91  		return fmt.Errorf("invalid action configuration: %w", err)
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  func (m *RuntimeManifest) loadBundle() error {
    98  	// Load the CNAB representation of the bundle
    99  	b, err := cnab.LoadBundle(m.config.Context, "/cnab/bundle.json")
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	m.bundle = b
   105  	return nil
   106  }
   107  
   108  func (m *RuntimeManifest) GetInstallationNamespace() string {
   109  	return m.config.Getenv(config.EnvPorterInstallationNamespace)
   110  }
   111  
   112  func (m *RuntimeManifest) GetInstallationName() string {
   113  	return m.config.Getenv(config.EnvPorterInstallationName)
   114  }
   115  
   116  func (m *RuntimeManifest) loadDependencyDefinitions() error {
   117  	m.bundles = make(map[string]cnab.ExtendedBundle, len(m.Dependencies.Requires))
   118  	for _, dep := range m.Dependencies.Requires {
   119  		bunD, err := GetDependencyDefinition(m.config.Context, dep.Name)
   120  		if err != nil {
   121  			return err
   122  		}
   123  
   124  		bun, err := bundle.Unmarshal(bunD)
   125  		if err != nil {
   126  			return fmt.Errorf("error unmarshaling bundle definition for dependency %s: %w", dep.Name, err)
   127  		}
   128  
   129  		m.bundles[dep.Name] = cnab.NewBundle(*bun)
   130  	}
   131  
   132  	return nil
   133  }
   134  
   135  // This wrapper type serve as a way of formatting a `map[string]interface{}` as
   136  // JSON when the templating by mustache is done. It makes it possible to
   137  // maintain the JSON string representation of the map while still allowing the
   138  // map to be used as a context in the templating, allowing for accessing the
   139  // map's keys in the template.
   140  type FormattedObject map[string]interface{}
   141  
   142  // Format the `FormattedObject` as a JSON string.
   143  func (fo FormattedObject) Format(f fmt.State, c rune) {
   144  	jsonStr, _ := json.Marshal(fo)
   145  	fmt.Fprintf(f, "%s", string(jsonStr))
   146  }
   147  
   148  func (m *RuntimeManifest) resolveParameter(pd manifest.ParameterDefinition) (interface{}, error) {
   149  	getValue := func(envVar string) (interface{}, error) {
   150  		value := m.config.Getenv(envVar)
   151  		if pd.Type == "object" {
   152  			var obj map[string]interface{}
   153  			if err := json.Unmarshal([]byte(value), &obj); err != nil {
   154  				return nil, err
   155  			}
   156  			return FormattedObject(obj), nil
   157  		}
   158  		return value, nil
   159  	}
   160  
   161  	if pd.Destination.EnvironmentVariable != "" {
   162  		return getValue(pd.Destination.EnvironmentVariable)
   163  	}
   164  	if pd.Destination.Path != "" {
   165  		return pd.Destination.Path, nil
   166  	}
   167  	envVar := manifest.ParamToEnvVar(pd.Name)
   168  	return getValue(envVar)
   169  }
   170  
   171  func (m *RuntimeManifest) resolveCredential(cd manifest.CredentialDefinition) (string, error) {
   172  	if cd.EnvironmentVariable != "" {
   173  		return m.config.Getenv(cd.EnvironmentVariable), nil
   174  	} else if cd.Path != "" {
   175  		return cd.Path, nil
   176  	} else {
   177  		return "", fmt.Errorf("credential: %s is malformed", cd.Name)
   178  	}
   179  }
   180  
   181  func (m *RuntimeManifest) resolveBundleOutput(outputName string) (string, error) {
   182  	// Get the output's value from the injected parameter source
   183  	ps := manifest.GetParameterSourceForOutput(outputName)
   184  	psParamEnv := manifest.ParamToEnvVar(ps)
   185  	outputValue, ok := m.config.LookupEnv(psParamEnv)
   186  	if !ok {
   187  		return "", fmt.Errorf("no parameter source was injected for output %s", outputName)
   188  	}
   189  	return outputValue, nil
   190  }
   191  
   192  func (m *RuntimeManifest) GetSensitiveValues() []string {
   193  	if m.sensitiveValues == nil {
   194  		return []string{}
   195  	}
   196  	return m.sensitiveValues
   197  }
   198  
   199  func (m *RuntimeManifest) setSensitiveValue(val string) {
   200  	exists := false
   201  	for _, item := range m.sensitiveValues {
   202  		if item == val {
   203  			exists = true
   204  		}
   205  	}
   206  
   207  	if !exists {
   208  		m.sensitiveValues = append(m.sensitiveValues, val)
   209  	}
   210  }
   211  
   212  func (m *RuntimeManifest) GetSteps() manifest.Steps {
   213  	return m.steps
   214  }
   215  
   216  func (m *RuntimeManifest) GetOutputs() map[string]string {
   217  	outputs := make(map[string]string, len(m.outputs))
   218  
   219  	for k, v := range m.outputs {
   220  		outputs[k] = v
   221  	}
   222  
   223  	return outputs
   224  }
   225  
   226  func (m *RuntimeManifest) setStepsByAction() error {
   227  	switch m.Action {
   228  	case cnab.ActionInstall:
   229  		m.steps = m.Install
   230  	case cnab.ActionUninstall:
   231  		m.steps = m.Uninstall
   232  	case cnab.ActionUpgrade:
   233  		m.steps = m.Upgrade
   234  	default:
   235  		customAction, ok := m.CustomActions[m.Action]
   236  		if !ok {
   237  			actions := make([]string, 0, len(m.CustomActions))
   238  			for a := range m.CustomActions {
   239  				actions = append(actions, a)
   240  			}
   241  			return fmt.Errorf("unsupported action %q, custom actions are defined for: %s", m.Action, strings.Join(actions, ", "))
   242  		}
   243  		m.steps = customAction
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  func (m *RuntimeManifest) ApplyStepOutputs(assignments map[string]string) error {
   250  	if m.outputs == nil {
   251  		m.outputs = map[string]string{}
   252  	}
   253  
   254  	for outvar, outval := range assignments {
   255  		m.outputs[outvar] = outval
   256  	}
   257  	return nil
   258  }
   259  
   260  type StepOutput struct {
   261  	// The final value of the output returned by the mixin after executing
   262  	//lint:ignore U1000 ignore unused warning
   263  	value string
   264  
   265  	Name string                 `yaml:"name"`
   266  	Data map[string]interface{} `yaml:",inline"`
   267  }
   268  
   269  func (m *RuntimeManifest) buildSourceData() (map[string]interface{}, error) {
   270  	data := make(map[string]interface{})
   271  	m.sensitiveValues = []string{}
   272  
   273  	inst := make(map[string]interface{})
   274  	data["installation"] = inst
   275  	inst["namespace"] = m.GetInstallationNamespace()
   276  	inst["name"] = m.GetInstallationName()
   277  
   278  	bun := make(map[string]interface{})
   279  	data["bundle"] = bun
   280  
   281  	// Enable interpolation of manifest/bundle name via bundle.name
   282  	bun["name"] = m.Name
   283  	bun["version"] = m.Version
   284  	bun["description"] = m.Description
   285  	bun["installerImage"] = m.Image
   286  	bun["custom"] = m.Custom
   287  
   288  	// Make environment variable accessible
   289  	env := m.config.EnvironMap()
   290  	data["env"] = env
   291  
   292  	params := make(map[string]interface{})
   293  	bun["parameters"] = params
   294  	for _, param := range m.Parameters {
   295  		if !param.AppliesTo(m.Action) {
   296  			continue
   297  		}
   298  
   299  		pe := param.Name
   300  		val, err := m.resolveParameter(param)
   301  		if err != nil {
   302  			return nil, err
   303  		}
   304  		if param.Sensitive {
   305  			m.setSensitiveValue(fmt.Sprint(val))
   306  		}
   307  		params[pe] = val
   308  	}
   309  
   310  	creds := make(map[string]interface{})
   311  	bun["credentials"] = creds
   312  	for _, cred := range m.Credentials {
   313  		pe := cred.Name
   314  		val, err := m.resolveCredential(cred)
   315  		if err != nil {
   316  			return nil, err
   317  		}
   318  		m.setSensitiveValue(val)
   319  		creds[pe] = val
   320  	}
   321  
   322  	deps := make(map[string]interface{})
   323  	bun["dependencies"] = deps
   324  	for alias, depB := range m.bundles {
   325  		// bundle.dependencies.ALIAS.outputs.NAME
   326  		depBun := make(map[string]interface{})
   327  		deps[alias] = depBun
   328  
   329  		depBun["name"] = depB.Name
   330  		depBun["version"] = depB.Version
   331  		depBun["description"] = depB.Description
   332  	}
   333  
   334  	bun["outputs"] = m.outputs
   335  
   336  	// Iterate through the runtime manifest's step outputs and determine if we should mask
   337  	for name, val := range m.outputs {
   338  		// TODO: support configuring sensitivity for step outputs that aren't also bundle-level outputs
   339  		// See https://github.com/getporter/porter/issues/855
   340  
   341  		// If step output is also a bundle-level output, defer to bundle-level output sensitivity
   342  		if outputDef, ok := m.Outputs[name]; ok && !outputDef.Sensitive {
   343  			continue
   344  		}
   345  		m.setSensitiveValue(val)
   346  	}
   347  
   348  	// Externally injected outputs (bundle level outputs and dependency outputs) are
   349  	// injected through parameter sources.
   350  	bunExt, err := m.bundle.ProcessRequiredExtensions()
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	paramSources, _, err := bunExt.GetParameterSources()
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	templatedOutputs := m.GetTemplatedOutputs()
   361  	templatedDependencyOutputs := m.GetTemplatedDependencyOutputs()
   362  	for paramName, sources := range paramSources {
   363  		param := m.bundle.Parameters[paramName]
   364  		if !param.AppliesTo(m.Action) {
   365  			continue
   366  		}
   367  
   368  		for _, s := range sources.ListSourcesByPriority() {
   369  			switch ps := s.(type) {
   370  			case cnab.DependencyOutputParameterSource:
   371  				outRef := manifest.DependencyOutputReference{Dependency: ps.Dependency, Output: ps.OutputName}
   372  
   373  				// Ignore anything that isn't templated, because that's what we are building the source data for
   374  				if _, isTemplated := templatedDependencyOutputs[outRef.String()]; !isTemplated {
   375  					continue
   376  				}
   377  
   378  				depBun := deps[ps.Dependency].(map[string]interface{})
   379  				var depOutputs map[string]interface{}
   380  				if depBun["outputs"] == nil {
   381  					depOutputs = make(map[string]interface{})
   382  					depBun["outputs"] = depOutputs
   383  				} else {
   384  					depOutputs = depBun["outputs"].(map[string]interface{})
   385  				}
   386  
   387  				value, err := m.ReadDependencyOutputValue(outRef)
   388  				if err != nil {
   389  					return nil, err
   390  				}
   391  
   392  				depOutputs[ps.OutputName] = value
   393  
   394  				// Determine if the dependency's output is defined as sensitive
   395  				depB := m.bundles[ps.Dependency]
   396  				if ok, _ := depB.IsOutputSensitive(ps.OutputName); ok {
   397  					m.setSensitiveValue(value)
   398  				}
   399  
   400  			case cnab.OutputParameterSource:
   401  				// Ignore anything that isn't templated, because that's what we are building the source data for
   402  				if _, isTemplated := templatedOutputs[ps.OutputName]; !isTemplated {
   403  					continue
   404  				}
   405  
   406  				// A bundle-level output may also be a step-level output
   407  				// If already set, do not override
   408  				if val, exists := m.outputs[ps.OutputName]; exists && val != "" {
   409  					continue
   410  				}
   411  
   412  				val, err := m.resolveBundleOutput(ps.OutputName)
   413  				if err != nil {
   414  					return nil, err
   415  				}
   416  
   417  				if m.outputs == nil {
   418  					m.outputs = map[string]string{}
   419  				}
   420  				m.outputs[ps.OutputName] = val
   421  				bun["outputs"] = m.outputs
   422  
   423  				outputDef := m.Manifest.Outputs[ps.OutputName]
   424  				if outputDef.Sensitive {
   425  					m.setSensitiveValue(val)
   426  				}
   427  			}
   428  		}
   429  	}
   430  
   431  	images := make(map[string]interface{})
   432  	bun["images"] = images
   433  	for alias, image := range m.ImageMap {
   434  		// just assigning the struct here results in uppercase keys, which would give us
   435  		// strange things like {{ bundle.images.something.Repository }}
   436  		// So reflect and walk through the struct (this way we don't need to update this later)
   437  		val := reflect.ValueOf(image)
   438  		img := make(map[string]string)
   439  		typeOfT := val.Type()
   440  		for i := 0; i < val.NumField(); i++ {
   441  			f := val.Field(i)
   442  			name := toCamelCase(typeOfT.Field(i).Name)
   443  			img[name] = f.String()
   444  		}
   445  		images[alias] = img
   446  	}
   447  	return data, nil
   448  }
   449  
   450  func (m *RuntimeManifest) buildAndResolveMappedDependencyOutputs(sourceData map[string]interface{}) error {
   451  	for _, manifestDep := range m.Dependencies.Requires {
   452  		var depBun = sourceData["bundle"].(map[string]interface{})["dependencies"].(map[string]interface{})[manifestDep.Name].(map[string]interface{})
   453  		var depOutputs map[string]interface{}
   454  		if depBun["outputs"] == nil {
   455  			depOutputs = make(map[string]interface{})
   456  			depBun["outputs"] = depOutputs
   457  		} else {
   458  			depOutputs = depBun["outputs"].(map[string]interface{})
   459  		}
   460  
   461  		sourceData["outputs"] = depOutputs
   462  
   463  		for outputName, mappedOutput := range manifestDep.Outputs {
   464  			mappedOutputTemplate := m.GetTemplatePrefix() + mappedOutput
   465  			renderedOutput, err := mustache.RenderRaw(mappedOutputTemplate, true, sourceData)
   466  			if err != nil {
   467  				return fmt.Errorf("unable to render dependency %s output template %s: %w", manifestDep.Name, mappedOutput, err)
   468  			}
   469  			depOutputs[outputName] = renderedOutput
   470  		}
   471  	}
   472  
   473  	delete(sourceData, "outputs")
   474  
   475  	return nil
   476  }
   477  
   478  // ResolveStep will walk through the Step's data and resolve any placeholder
   479  // data using the definitions in the manifest, like parameters or credentials.
   480  func (m *RuntimeManifest) ResolveStep(ctx context.Context, stepIndex int, step *manifest.Step) error {
   481  	log := tracing.LoggerFromContext(ctx)
   482  
   483  	// Refresh our template data
   484  	sourceData, err := m.buildSourceData()
   485  	if err != nil {
   486  		return log.Error(fmt.Errorf("unable to build step template data: %w", err))
   487  	}
   488  
   489  	mustache.AllowMissingVariables = false
   490  
   491  	if m.config.IsFeatureEnabled(experimental.FlagDependenciesV2) {
   492  		err = m.buildAndResolveMappedDependencyOutputs(sourceData)
   493  		if err != nil {
   494  			return log.Errorf("unable to build and resolve mapped dependency outputs: %w", err)
   495  		}
   496  	}
   497  
   498  	// Get the original yaml for the current step
   499  	stepPath := fmt.Sprintf("%s[%d]", m.Action, stepIndex)
   500  	stepTemplate, err := m.getStepTemplate(stepPath)
   501  	if err != nil {
   502  		return log.Error(fmt.Errorf("unable to retrieve original yaml for step %s: %w", stepPath, err))
   503  	}
   504  
   505  	// TODO: add back logging step data after we have a solid way to censor it in https://github.com/getporter/porter/issues/2256
   506  	//fmt.Fprintf(m.Err, "=== Step Data ===\n%v\n", sourceData)
   507  	m.debugf(log, "=== Step Template ===\n%v\n", stepTemplate)
   508  
   509  	rendered, err := mustache.RenderRaw(stepTemplate, true, sourceData)
   510  	if err != nil {
   511  		return log.Errorf("unable to render step template %s: %w", stepTemplate, err)
   512  	}
   513  
   514  	// TODO: add back logging step data after we have a solid way to censor it in https://github.com/getporter/porter/issues/2256
   515  	//fmt.Fprintf(m.Err, "=== Rendered Step ===\n%s\n", rendered)
   516  
   517  	// Update the step parameter with the result of rendering the template
   518  	err = yaml.Unmarshal([]byte(rendered), step)
   519  	if err != nil {
   520  		return log.Error(fmt.Errorf("invalid step yaml after rendering template\n%s: %w", stepTemplate, err))
   521  	}
   522  
   523  	return nil
   524  }
   525  
   526  // Initialize prepares the runtime environment prior to step execution
   527  func (m *RuntimeManifest) Initialize(ctx context.Context) error {
   528  	if err := m.createOutputsDir(); err != nil {
   529  		return err
   530  	}
   531  
   532  	// For parameters of type "file", we may need to decode files on the filesystem
   533  	// before execution of the step/action
   534  	for paramName, param := range m.bundle.Parameters {
   535  		if !param.AppliesTo(m.Action) {
   536  			continue
   537  		}
   538  
   539  		def, hasDef := m.bundle.Definitions[param.Definition]
   540  		if !hasDef {
   541  			continue
   542  		}
   543  
   544  		if m.bundle.IsFileType(def) {
   545  			if param.Destination.Path == "" {
   546  				return fmt.Errorf("destination path is not supplied for parameter %s", paramName)
   547  			}
   548  
   549  			// Porter by default places parameter value into file determined by Destination.Path
   550  			bytes, err := m.config.FileSystem.ReadFile(param.Destination.Path)
   551  			if err != nil {
   552  				if os.IsNotExist(err) {
   553  					continue
   554  				}
   555  				return fmt.Errorf("unable to acquire value for parameter %s: %w", paramName, err)
   556  			}
   557  
   558  			// TODO(carolynvs): hack around parameters ALWAYS being injected even when empty files mess things up
   559  			// I'm not sure yet why it's injecting as null instead of "" (as required by the spec)
   560  			// We want to get the null -> "" fixed, and also not write files into the bundle when unset.
   561  			// that's a cnab change somewhere probably
   562  			// the problem is in injectParameters in cnab-go
   563  			if string(bytes) == "null" {
   564  				err := m.config.FileSystem.Remove(param.Destination.Path)
   565  				if err != nil {
   566  					return fmt.Errorf("unable to remove destination path: %w", err)
   567  				}
   568  				continue
   569  			}
   570  			decoded, err := base64.StdEncoding.DecodeString(string(bytes))
   571  			if err != nil {
   572  				return fmt.Errorf("unable to decode parameter %s: %w", paramName, err)
   573  			}
   574  
   575  			err = m.config.FileSystem.WriteFile(param.Destination.Path, decoded, pkg.FileModeWritable)
   576  			if err != nil {
   577  				return fmt.Errorf("unable to write decoded parameter %s: %w", paramName, err)
   578  			}
   579  		}
   580  	}
   581  
   582  	return m.unpackStateBag(ctx)
   583  }
   584  
   585  func (m *RuntimeManifest) createOutputsDir() error {
   586  	// Ensure outputs directory exists
   587  	if err := m.config.FileSystem.MkdirAll(config.BundleOutputsDir, pkg.FileModeDirectory); err != nil {
   588  		return fmt.Errorf("unable to ensure CNAB outputs directory exists: %w", err)
   589  	}
   590  	return nil
   591  }
   592  
   593  // Unpack each state variable from /porter/state.tgz and copy it to its
   594  // declared location in the bundle.
   595  func (m *RuntimeManifest) unpackStateBag(ctx context.Context) error {
   596  	log := tracing.LoggerFromContext(ctx)
   597  	_, err := m.config.FileSystem.Open(statePath)
   598  	if os.IsNotExist(err) || len(m.StateBag) == 0 {
   599  		m.debugf(log, "No existing bundle state to unpack")
   600  		return nil
   601  	}
   602  	bytes, err := m.config.FileSystem.ReadFile(statePath)
   603  	if err != nil {
   604  		m.debugf(log, "Unable to read bundle state file")
   605  		return err
   606  	}
   607  	// TODO(sgettys): hack around state.tgz ALWAYS being injected even when empty files mess things up
   608  	// I'm not sure yet why it's injecting as null instead of "" (as required by the spec)
   609  	// We want to get the null -> "" fixed, and also not write files into the bundle when unset.
   610  	// that's a cnab change somewhere probably
   611  	// the problem is in injectParameters in cnab-go
   612  	if string(bytes) == "null" {
   613  		m.debugf(log, "Bundle state file has null content")
   614  		err = m.config.FileSystem.Remove(statePath)
   615  		if err != nil {
   616  			_ = log.Error(err)
   617  		}
   618  		return nil
   619  	}
   620  	// Unpack the state file and copy its contents to where the bundle expects them
   621  	// state var name -> path in bundle
   622  	log.Debug("Unpacking bundle state...")
   623  	stateFiles := make(map[string]string, len(m.StateBag))
   624  	for _, s := range m.StateBag {
   625  		stateFiles[s.Name] = s.Path
   626  	}
   627  
   628  	unpackStateFile := func(tr *tar.Reader, header *tar.Header) error {
   629  		name := strings.TrimPrefix(header.Name, "porter-state/")
   630  		dest := stateFiles[name]
   631  		m.debugf(log, "  - %s -> %s", name, dest)
   632  
   633  		f, err := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
   634  		if err != nil {
   635  			return log.Error(fmt.Errorf("error creating state file %s: %w", dest, err))
   636  		}
   637  		defer f.Close()
   638  
   639  		_, err = io.Copy(f, tr)
   640  		if err != nil {
   641  			return log.Error(fmt.Errorf("error unpacking state file %s: %w", dest, err))
   642  		}
   643  
   644  		return nil
   645  	}
   646  
   647  	stateArchive, err := m.config.FileSystem.Open(statePath)
   648  	if err != nil {
   649  		return log.Error(fmt.Errorf("could not open statefile at %s: %w", statePath, err))
   650  	}
   651  
   652  	gzr, err := gzip.NewReader(stateArchive)
   653  	if err != nil {
   654  		if err == io.EOF {
   655  			log.Debug("statefile exists but is empty")
   656  			return nil
   657  		}
   658  		return log.Error(fmt.Errorf("could not create a new gzip reader for the statefile: %w", err))
   659  	}
   660  	defer gzr.Close()
   661  
   662  	tr := tar.NewReader(gzr)
   663  	for {
   664  		header, err := tr.Next()
   665  		if err != nil {
   666  			if err == io.EOF {
   667  				break
   668  			}
   669  		} else if header.Typeflag != tar.TypeReg {
   670  			continue
   671  		}
   672  
   673  		if err = unpackStateFile(tr, header); err != nil {
   674  			return err
   675  		}
   676  	}
   677  
   678  	return nil
   679  }
   680  
   681  // Finalize cleans up the bundle before its completion.
   682  func (m *RuntimeManifest) Finalize(ctx context.Context) error {
   683  	var bigErr *multierror.Error
   684  
   685  	if err := m.applyUnboundBundleOutputs(ctx); err != nil {
   686  		bigErr = multierror.Append(bigErr, err)
   687  	}
   688  
   689  	// Always try to persist state, even when errors occur
   690  	if err := m.packStateBag(ctx); err != nil {
   691  		bigErr = multierror.Append(bigErr, err)
   692  	}
   693  
   694  	return bigErr.ErrorOrNil()
   695  }
   696  
   697  // Pack each state variable into /porter/state/tgz.
   698  func (m *RuntimeManifest) packStateBag(ctx context.Context) error {
   699  	log := tracing.LoggerFromContext(ctx)
   700  
   701  	m.debugf(log, "Packing bundle state...")
   702  	packStateFile := func(tw *tar.Writer, s manifest.StateVariable) error {
   703  		fi, err := m.config.FileSystem.Stat(s.Path)
   704  		if os.IsNotExist(err) {
   705  			return nil
   706  		}
   707  
   708  		m.debugf(log, "  - %s", s.Path)
   709  		header, err := tar.FileInfoHeader(fi, fi.Name())
   710  		if err != nil {
   711  			return log.Error(fmt.Errorf("error creating tar header for state variable %s from path %s: %w", s.Name, s.Path, err))
   712  		}
   713  		header.Name = filepath.Join("porter-state", s.Name)
   714  
   715  		if err := tw.WriteHeader(header); err != nil {
   716  			return log.Error(fmt.Errorf("error writing tar header for state variable %s: %w", s.Name, err))
   717  		}
   718  
   719  		f, err := os.Open(s.Path)
   720  		if err != nil {
   721  			return log.Error(fmt.Errorf("error reading state file %s for variable %s: %w", s.Path, s.Name, err))
   722  		}
   723  		defer f.Close()
   724  
   725  		_, err = io.Copy(tw, f)
   726  		if err != nil {
   727  			return log.Error(fmt.Errorf("error archiving state file %s for variable %s: %w", s.Path, s.Name, err))
   728  		}
   729  
   730  		return nil
   731  	}
   732  
   733  	// Save directly to the final output location since we've already collected outputs at this point
   734  	stateArchive, err := m.config.FileSystem.Create("/cnab/app/outputs/porter-state")
   735  	if err != nil {
   736  		return log.Error(fmt.Errorf("error creating porter statefile: %w", err))
   737  	}
   738  
   739  	gzw := gzip.NewWriter(stateArchive)
   740  	defer gzw.Close()
   741  
   742  	tw := tar.NewWriter(gzw)
   743  	defer tw.Close()
   744  
   745  	// Persist as many of the state vars as possible, even if one fails
   746  	var bigErr *multierror.Error
   747  	for _, s := range m.StateBag {
   748  		err := packStateFile(tw, s)
   749  		if err != nil {
   750  			bigErr = multierror.Append(bigErr, err)
   751  		}
   752  	}
   753  
   754  	return log.Error(bigErr.ErrorOrNil())
   755  }
   756  
   757  // applyUnboundBundleOutputs finds outputs that haven't been bound yet by a step,
   758  // and if they can be bound, i.e. they grab a file from the bundle's filesystem,
   759  // apply the output.
   760  func (m *RuntimeManifest) applyUnboundBundleOutputs(ctx context.Context) error {
   761  	if len(m.bundle.Outputs) == 0 {
   762  		return nil
   763  	}
   764  
   765  	log := tracing.LoggerFromContext(ctx)
   766  
   767  	log.Debug("Collecting bundle outputs...")
   768  	var bigErr *multierror.Error
   769  	outputs := m.GetOutputs()
   770  	for name, outputDef := range m.bundle.Outputs {
   771  		outputSrcPath := m.Outputs[name].Path
   772  
   773  		// We can only deal with outputs that are based on a file right now
   774  		if outputSrcPath == "" {
   775  			continue
   776  		}
   777  
   778  		if !outputDef.AppliesTo(m.Action) {
   779  			continue
   780  		}
   781  
   782  		// Print the output that we've collected
   783  		m.debugf(log, "  - %s", name)
   784  		if _, hasOutput := outputs[name]; !hasOutput {
   785  			// Use the path as originally defined in the manifest
   786  			// TODO(carolynvs): When we switch to driving everything completely
   787  			// from the bundle.json, we need to find a better way to get the original path value that the user specified.
   788  			// We don't force people to output files immediately to /cnab/app/outputs and so that original location should
   789  			// be persisted somewhere in the bundle.json (probably in custom)
   790  			srcPath := manifest.ResolvePath(outputSrcPath)
   791  			if _, err := m.config.FileSystem.Stat(srcPath); err != nil {
   792  				continue
   793  			}
   794  			dstPath := filepath.Join(config.BundleOutputsDir, name)
   795  			if dstExists, _ := m.config.FileSystem.Exists(dstPath); dstExists {
   796  				continue
   797  			}
   798  
   799  			err := m.config.CopyFile(srcPath, dstPath)
   800  			if err != nil {
   801  				bigErr = multierror.Append(bigErr, fmt.Errorf("unable to copy output file from %s to %s: %w", srcPath, dstPath, err))
   802  				continue
   803  			}
   804  		}
   805  	}
   806  
   807  	return log.Error(bigErr.ErrorOrNil())
   808  }
   809  
   810  // ResolveInvocationImage updates the RuntimeManifest to properly reflect the bundle image passed to the bundle via the
   811  // mounted bundle.json and relocation mapping
   812  func (m *RuntimeManifest) ResolveInvocationImage(bun cnab.ExtendedBundle, reloMap relocation.ImageRelocationMap) error {
   813  	for _, image := range bun.InvocationImages {
   814  		if image.Digest == "" || image.ImageType != "docker" {
   815  			continue
   816  		}
   817  
   818  		ref, err := cnab.ParseOCIReference(image.Image)
   819  		if err != nil {
   820  			return fmt.Errorf("unable to parse bundle image reference: %w", err)
   821  		}
   822  		refWithDigest, err := ref.WithDigest(digest.Digest(image.Digest))
   823  		if err != nil {
   824  			return fmt.Errorf("unable to get bundle image reference with digest: %w", err)
   825  		}
   826  
   827  		m.Image = refWithDigest.String()
   828  		break
   829  	}
   830  	relocated, ok := reloMap[m.Image]
   831  	if ok {
   832  		m.Image = relocated
   833  	}
   834  	return nil
   835  }
   836  
   837  // ResolveImages updates the RuntimeManifest to properly reflect the image map passed to the bundle via the
   838  // mounted bundle.json and relocation mapping
   839  func (m *RuntimeManifest) ResolveImages(bun cnab.ExtendedBundle, reloMap relocation.ImageRelocationMap) error {
   840  	// It only makes sense to process this if the runtime manifest has images defined. If none are defined
   841  	// return early
   842  	if len(m.ImageMap) == 0 {
   843  		return nil
   844  	}
   845  	reverseLookup := make(map[string]string)
   846  	for alias, image := range bun.Images {
   847  		manifestImage, ok := m.ImageMap[alias]
   848  		if !ok {
   849  			return fmt.Errorf("unable to find image in porter manifest: %s", alias)
   850  		}
   851  		manifestImage.Digest = image.Digest
   852  		err := resolveImage(&manifestImage, image.Image)
   853  		if err != nil {
   854  			return fmt.Errorf("unable to update image map from bundle.json: %w", err)
   855  		}
   856  		m.ImageMap[alias] = manifestImage
   857  		reverseLookup[image.Image] = alias
   858  	}
   859  	for oldRef, reloRef := range reloMap {
   860  		alias := reverseLookup[oldRef]
   861  		if manifestImage, ok := m.ImageMap[alias]; ok { //note, there might be other images in the relocation mapping, like the bundle image
   862  			err := resolveImage(&manifestImage, reloRef)
   863  			if err != nil {
   864  				return fmt.Errorf("unable to update image map from relocation mapping: %w", err)
   865  			}
   866  			m.ImageMap[alias] = manifestImage
   867  		}
   868  	}
   869  	return nil
   870  }
   871  
   872  func (m *RuntimeManifest) getEditor() (*yaml.Editor, error) {
   873  	if m.editor != nil {
   874  		return m.editor, nil
   875  	}
   876  
   877  	// Get the original porter.yaml with additional yaml metadata so that we can look at just the current step's yaml
   878  	yq := yaml.NewEditor(m.config.FileSystem)
   879  	if err := yq.ReadFile(m.ManifestPath); err != nil {
   880  		return nil, fmt.Errorf("error loading yaml editor from %s", m.ManifestPath)
   881  	}
   882  
   883  	return yq, nil
   884  }
   885  
   886  func (m *RuntimeManifest) getStepTemplate(stepPath string) (string, error) {
   887  	yq, err := m.getEditor()
   888  	if err != nil {
   889  		return "", err
   890  	}
   891  
   892  	stepNode, err := yq.GetNode(stepPath)
   893  	if err != nil {
   894  		return "", fmt.Errorf("unable to retrieve original yaml for step %s: %w", stepPath, err)
   895  	}
   896  
   897  	var stepYAML bytes.Buffer
   898  	enc := yaml3.NewEncoder(&stepYAML)
   899  	defer enc.Close()
   900  	if err := enc.Encode(stepNode); err != nil {
   901  		return "", fmt.Errorf("error re-encoding porter.yaml for templating: %w", err)
   902  	}
   903  
   904  	stepTemplate := m.GetTemplatePrefix() + stepYAML.String()
   905  	return stepTemplate, nil
   906  }
   907  
   908  func resolveImage(image *manifest.MappedImage, refString string) error {
   909  	//figure out what type of Reference it is so we can extract useful things for our image map
   910  	ref, err := cnab.ParseOCIReference(refString)
   911  	if err != nil {
   912  		return err
   913  	}
   914  
   915  	image.Repository = ref.Repository()
   916  	if ref.HasDigest() {
   917  		image.Digest = ref.Digest().String()
   918  	}
   919  
   920  	if ref.HasTag() {
   921  		image.Tag = ref.Tag()
   922  	}
   923  
   924  	if ref.IsRepositoryOnly() {
   925  		image.Tag = "latest" // Populate this with latest so that the {{ can reference something }}
   926  	}
   927  
   928  	return nil
   929  }
   930  
   931  // toCamelCase returns a camel-cased variant of the provided string
   932  func toCamelCase(str string) string {
   933  	var b strings.Builder
   934  
   935  	b.WriteString(strings.ToLower(string(str[0])))
   936  	b.WriteString(str[1:])
   937  
   938  	return b.String()
   939  }