Repository: getporter/porter
Pull request #1974 (2022-03-15 18:49:02)

Do not assume that all bundles have a supported porter.yaml file
# What does this change
These are changes necessary to have `porter explain` and `porter inspect` work with bundles created with an unsupported porter version.

**Do not require a supported porter.yaml file in the bundle cache**

It's important that we should be able to pull a bundle and fall back to working with it as a vanilla cnab bundle when it was built with an unsupported porter version. This removes trying to load the manifest from the bundle cache and each calling command will need to decide how to gracefully handle the bundle it pulls from the cache (i.e. some commands have to require a supported version, like build, but others like install can still work without a manifest).

**Remove Porter.Manifest field**
The porter struct has always had a Manifest field that is great at causing nil pointer exceptions (which is why I also fixed Porter to print out panic stack traces) because it's essentially a global variable and not all code paths populate it.

Since we need to support bundles that may not be able to populate a manifest, i.e. it's an unsupported manifest schema, or a vanilla CNAB bundle, I've removed it and made any commands that need the manifest to load it themselves instead of relying on this global.

# What issue does it fix
Part of #1956 

# Notes for the reviewer
N/A

# Checklist
- [ ] Did you write tests?
- [ ] Did you write documentation?
- [ ] Did you change porter.yaml or a storage document record? Update the corresponding schema file.
- [ ] If this is your first pull request, please add your name to the bottom of our [Contributors][contributors] list. Thank you for making Porter better! 🙇‍♀️

# Reviewer Checklist
* Comment with /azp run test-porter-release if a magefile or build script was modified
* Comment with /azp run porter-integration if it's a non-trivial PR

[contributors]: https://porter.sh/src/CONTRIBUTORS.md


Patch:
diff --git a/cmd/porter/main.go b/cmd/porter/main.go
index d6afc8c5..05f986fc 100644
--- a/cmd/porter/main.go
+++ b/cmd/porter/main.go
@@ -5,6 +5,7 @@ import (
 	_ "embed"
 	"fmt"
 	"os"
+	"runtime/debug"
 	"strings"
 
 	"get.porter.sh/porter/pkg/cli"
@@ -50,7 +51,9 @@ func main() {
 		defer func() {
 			// Capture panics and trace them
 			if panicErr := recover(); panicErr != nil {
-				log.Error(errors.New(fmt.Sprintf("%s", panicErr)), attribute.Bool("panic", true))
+				log.Error(errors.New(fmt.Sprintf("%s", panicErr)),
+					attribute.Bool("panic", true),
+					attribute.String("stackTrace", string(debug.Stack())))
 				log.EndSpan()
 				p.Close()
 				os.Exit(1)
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index 29f6539d..49efaa78 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -11,7 +11,6 @@ import (
 	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/encoding"
-	"get.porter.sh/porter/pkg/manifest"
 	"github.com/opencontainers/go-digest"
 	"github.com/pkg/errors"
 )
@@ -166,12 +165,6 @@ func (c *Cache) cacheManifest(cb *CachedBundle) error {
 		if err != nil {
 			return errors.Wrapf(err, "error writing porter.yaml for %s", cb.Reference)
 		}
-
-		m, err := manifest.LoadManifestFrom(c.Context, cb.ManifestPath)
-		if err != nil {
-			return errors.Wrapf(err, "error reading porter.yaml for %s", cb.Reference)
-		}
-		cb.Manifest = m
 	}
 
 	return nil
diff --git a/pkg/cache/cached_bundle.go b/pkg/cache/cached_bundle.go
index ef8673fe..211560d2 100644
--- a/pkg/cache/cached_bundle.go
+++ b/pkg/cache/cached_bundle.go
@@ -10,7 +10,6 @@ import (
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/context"
 	"get.porter.sh/porter/pkg/encoding"
-	"get.porter.sh/porter/pkg/manifest"
 	"github.com/cnabio/cnab-to-oci/relocation"
 	"github.com/pkg/errors"
 )
@@ -27,11 +26,6 @@ type CachedBundle struct {
 	// BundlePath is the location of the bundle.json in the cache.
 	BundlePath string
 
-	// Manifest is the optional porter.yaml manifest. This is only populated
-	// when the bundle was a porter built bundle that had a manifest embedded in
-	// the custom metadata.
-	Manifest *manifest.Manifest
-
 	// ManifestPath is the optional location of the porter.yaml in the cache.
 	ManifestPath string
 
@@ -76,14 +70,6 @@ func (cb *CachedBundle) BuildMetadataPath() string {
 func (cb *CachedBundle) Load(cxt *context.Context) (bool, error) {
 	// Check that the bundle exists
 	cb.BundlePath = cb.BuildBundlePath()
-	bundleExists, err := cxt.FileSystem.Exists(cb.BuildMetadataPath())
-	if err != nil {
-		return false, errors.Wrapf(err, "unable to read bundle %s at %s", cb.Reference, cb.BuildMetadataPath())
-	}
-	if !bundleExists {
-		return false, nil
-	}
-
 	metaPath := cb.BuildMetadataPath()
 	metaExists, err := cxt.FileSystem.Exists(metaPath)
 	if err != nil {
@@ -139,14 +125,6 @@ func (cb *CachedBundle) Load(cxt *context.Context) (bool, error) {
 		}
 		cb.RelocationMap = reloMap
 	}
-
-	if cb.ManifestPath != "" {
-		m, err := manifest.LoadManifestFrom(cxt, cb.ManifestPath)
-		if err != nil {
-			return true, errors.Wrapf(err, "unable to read cached manifest at %s", cb.ManifestPath)
-		}
-		cb.Manifest = m
-	}
-
+	
 	return true, nil
 }
diff --git a/pkg/cnab/config-adapter/helpers.go b/pkg/cnab/config-adapter/helpers.go
new file mode 100644
index 00000000..a607423f
--- /dev/null
+++ b/pkg/cnab/config-adapter/helpers.go
@@ -0,0 +1,15 @@
+package configadapter
+
+import (
+	"get.porter.sh/porter/pkg/cnab"
+	"get.porter.sh/porter/pkg/context"
+	"get.porter.sh/porter/pkg/manifest"
+)
+
+// ConvertToTestBundle is suitable for taking a test manifest (porter.yaml)
+// and making a bundle.json for it. Does not make an accurate representation
+// of the bundle, but is suitable for testing.
+func ConvertToTestBundle(cxt *context.Context, manifest *manifest.Manifest) (cnab.ExtendedBundle, error) {
+	converter := NewManifestConverter(cxt, manifest, nil, nil)
+	return converter.ToBundle()
+}
diff --git a/pkg/porter/build.go b/pkg/porter/build.go
index e7ab0636..fafb0b2c 100644
--- a/pkg/porter/build.go
+++ b/pkg/porter/build.go
@@ -10,6 +10,7 @@ import (
 	"get.porter.sh/porter/pkg/cnab"
 	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
 	"get.porter.sh/porter/pkg/config"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/mixin"
 	"get.porter.sh/porter/pkg/printer"
 	"github.com/Masterminds/semver/v3"
@@ -84,14 +85,15 @@ func (p *Porter) Build(ctx context.Context, opts BuildOptions) error {
 		return errors.Wrap(err, "unable to generate manifest")
 	}
 
-	if err := p.LoadManifestFrom(build.LOCAL_MANIFEST); err != nil {
+	m, err := manifest.LoadManifestFrom(p.Context, build.LOCAL_MANIFEST)
+	if err != nil {
 		return err
 	}
 
 	// Capture the path to the original, user-provided manifest.
 	// This value will be referenced elsewhere, for instance by
 	// the digest logic (to dictate auto-rebuild)
-	p.Manifest.ManifestPath = opts.File
+	m.ManifestPath = opts.File
 
 	if !opts.NoLint {
 		if err := p.preLint(); err != nil {
@@ -105,11 +107,11 @@ func (p *Porter) Build(ctx context.Context, opts BuildOptions) error {
 	// bundle.json will *not* be correct until the image is actually pushed
 	// to a registry.  The bundle.json will need to be updated after publishing
 	// and provided just-in-time during bundle execution.
-	if err := p.buildBundle(p.Manifest.Image, ""); err != nil {
+	if err := p.buildBundle(m, ""); err != nil {
 		return errors.Wrap(err, "unable to build bundle")
 	}
 
-	generator := build.NewDockerfileGenerator(p.Config, p.Manifest, p.Templates, p.Mixins)
+	generator := build.NewDockerfileGenerator(p.Config, m, p.Templates, p.Mixins)
 
 	if err := generator.PrepareFilesystem(); err != nil {
 		return fmt.Errorf("unable to copy run script, runtimes or mixins: %s", err)
@@ -119,7 +121,7 @@ func (p *Porter) Build(ctx context.Context, opts BuildOptions) error {
 	}
 
 	builder := p.GetBuilder()
-	return errors.Wrap(builder.BuildInvocationImage(ctx, p.Manifest), "unable to build CNAB invocation image")
+	return errors.Wrap(builder.BuildInvocationImage(ctx, m), "unable to build CNAB invocation image")
 }
 
 func (p *Porter) preLint() error {
@@ -150,7 +152,7 @@ func (p *Porter) preLint() error {
 	return nil
 }
 
-func (p *Porter) getUsedMixins() ([]mixin.Metadata, error) {
+func (p *Porter) getUsedMixins(m *manifest.Manifest) ([]mixin.Metadata, error) {
 	installedMixins, err := p.ListMixins()
 
 	if err != nil {
@@ -159,7 +161,7 @@ func (p *Porter) getUsedMixins() ([]mixin.Metadata, error) {
 
 	var usedMixins []mixin.Metadata
 	for _, installedMixin := range installedMixins {
-		for _, m := range p.Manifest.Mixins {
+		for _, m := range m.Mixins {
 			if installedMixin.Name == m.Name {
 				usedMixins = append(usedMixins, installedMixin)
 			}
@@ -169,16 +171,16 @@ func (p *Porter) getUsedMixins() ([]mixin.Metadata, error) {
 	return usedMixins, nil
 }
 
-func (p *Porter) buildBundle(invocationImage string, digest digest.Digest) error {
-	imageDigests := map[string]string{invocationImage: digest.String()}
+func (p *Porter) buildBundle(m *manifest.Manifest, digest digest.Digest) error {
+	imageDigests := map[string]string{m.Image: digest.String()}
 
-	mixins, err := p.getUsedMixins()
+	mixins, err := p.getUsedMixins(m)
 
 	if err != nil {
 		return err
 	}
 
-	converter := configadapter.NewManifestConverter(p.Context, p.Manifest, imageDigests, mixins)
+	converter := configadapter.NewManifestConverter(p.Context, m, imageDigests, mixins)
 	bun, err := converter.ToBundle()
 	if err != nil {
 		return err
diff --git a/pkg/porter/cnab.go b/pkg/porter/cnab.go
index 95280817..c028ba98 100644
--- a/pkg/porter/cnab.go
+++ b/pkg/porter/cnab.go
@@ -4,6 +4,7 @@ import (
 	"path/filepath"
 
 	"get.porter.sh/porter/pkg/build"
+	"get.porter.sh/porter/pkg/cnab"
 	"get.porter.sh/porter/pkg/cnab/drivers"
 	cnabprovider "get.porter.sh/porter/pkg/cnab/provider"
 	"get.porter.sh/porter/pkg/config"
@@ -239,7 +240,7 @@ func (o *bundleFileOptions) validateCNABFile(cxt *context.Context) error {
 
 // LoadParameters validates and resolves the parameters and sets. It must be
 // called after porter has loaded the bundle definition.
-func (o *sharedOptions) LoadParameters(p *Porter) error {
+func (o *sharedOptions) LoadParameters(p *Porter, bun cnab.ExtendedBundle) error {
 	// This is called in multiple code paths, so exit early if
 	// we have already loaded the parameters into combinedParameters
 	if o.combinedParameters != nil {
@@ -251,7 +252,7 @@ func (o *sharedOptions) LoadParameters(p *Porter) error {
 		return err
 	}
 
-	err = o.parseParamSets(p)
+	err = o.parseParamSets(p, bun)
 	if err != nil {
 		return err
 	}
@@ -272,9 +273,9 @@ func (o *sharedOptions) parseParams() error {
 }
 
 // parseParamSets parses the variable assignments in ParameterSets.
-func (o *sharedOptions) parseParamSets(p *Porter) error {
+func (o *sharedOptions) parseParamSets(p *Porter, bun cnab.ExtendedBundle) error {
 	if len(o.ParameterSets) > 0 {
-		parsed, err := p.loadParameterSets(o.Namespace, o.ParameterSets)
+		parsed, err := p.loadParameterSets(bun, o.Namespace, o.ParameterSets)
 		if err != nil {
 			return errors.Wrap(err, "unable to process provided parameter sets")
 		}
diff --git a/pkg/porter/dependencies.go b/pkg/porter/dependencies.go
index 38810627..d8d58eb9 100644
--- a/pkg/porter/dependencies.go
+++ b/pkg/porter/dependencies.go
@@ -20,7 +20,6 @@ type dependencyExecutioner struct {
 	*portercontext.Context
 	porter *Porter
 
-	Manifest *manifest.Manifest
 	Resolver BundleResolver
 	CNAB     cnabprovider.CNABProvider
 	Claims   claims.Provider
@@ -45,7 +44,6 @@ func newDependencyExecutioner(p *Porter, installation claims.Installation, actio
 		parentAction:       action,
 		parentOpts:         action.GetOptions(),
 		Context:            p.Context,
-		Manifest:           p.Manifest,
 		Resolver:           resolver,
 		CNAB:               p.CNAB,
 		Claims:             p.Claims,
@@ -209,7 +207,18 @@ func (e *dependencyExecutioner) prepareDependency(dep *queuedDependency) error {
 	//   - name: DEP
 	//     parameters:
 	//       PARAM: VALUE
-	for _, manifestDep := range e.Manifest.Dependencies.RequiredDependencies {
+	// TODO: When we redo dependencies, we need to remove this dependency on the bundle being a porter bundle with a manifest
+	// Yes, right now the way this works means this feature is Porter only
+	m := &manifest.Manifest{}
+	if e.parentOpts.File != "" {
+		var err error
+		m, err = manifest.LoadManifestFrom(e.Context, e.parentOpts.File)
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, manifestDep := range m.Dependencies.RequiredDependencies {
 		if manifestDep.Name == dep.Alias {
 			for paramName, value := range manifestDep.Parameters {
 				// Make sure the parameter is defined in the bundle
diff --git a/pkg/porter/install.go b/pkg/porter/install.go
index 1c486df7..dfc3a1f8 100644
--- a/pkg/porter/install.go
+++ b/pkg/porter/install.go
@@ -99,7 +99,7 @@ func (p *Porter) InstallBundle(ctx context.Context, opts InstallOptions) error {
 // Users are expected to edit the installation record if they don't want that behavior.
 func (p *Porter) applyActionOptionsToInstallation(i *claims.Installation, opts *BundleActionOptions) error {
 	// Record the parameters specified by the user, with flags taking precedence over parameter set values
-	err := opts.LoadParameters(p)
+	err := opts.LoadParameters(p, opts.bundleRef.Definition)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/porter/lifecycle.go b/pkg/porter/lifecycle.go
index 52012112..99e9f129 100644
--- a/pkg/porter/lifecycle.go
+++ b/pkg/porter/lifecycle.go
@@ -180,7 +180,7 @@ func (p *Porter) BuildActionArgs(ctx context.Context, installation claims.Instal
 
 	// Resolve the final set of typed parameters, taking into account the user overrides, parameter sources
 	// and defaults
-	err = opts.LoadParameters(p)
+	err = opts.LoadParameters(p, bundleRef.Definition)
 	if err != nil {
 		return cnabprovider.ActionArguments{}, err
 	}
@@ -234,9 +234,5 @@ func (p *Porter) prepullBundleByReference(opts *BundleActionOptions) (cache.Cach
 		opts.Name = cachedBundle.Definition.Name
 	}
 
-	if cachedBundle.Manifest != nil {
-		p.Manifest = cachedBundle.Manifest
-	}
-
 	return cachedBundle, nil
 }
diff --git a/pkg/porter/lint.go b/pkg/porter/lint.go
index 33d7d6f8..09496218 100644
--- a/pkg/porter/lint.go
+++ b/pkg/porter/lint.go
@@ -6,6 +6,7 @@ import (
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/context"
 	"get.porter.sh/porter/pkg/linter"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/printer"
 	"github.com/pkg/errors"
 )
@@ -57,13 +58,13 @@ func (o *LintOptions) validateFile(cxt *context.Context) error {
 func (p *Porter) Lint(opts LintOptions) (linter.Results, error) {
 	opts.Apply(p.Context)
 
-	err := p.LoadManifest()
+	manifest, err := manifest.LoadManifestFrom(p.Context, opts.File)
 	if err != nil {
 		return nil, err
 	}
 
 	l := linter.New(p.Context, p.Mixins)
-	return l.Lint(p.Manifest)
+	return l.Lint(manifest)
 }
 
 // PrintLintResults lints the manifest and prints the results to the attached output.
diff --git a/pkg/porter/options.go b/pkg/porter/options.go
index 3333b52d..346f8757 100644
--- a/pkg/porter/options.go
+++ b/pkg/porter/options.go
@@ -1,6 +1,7 @@
 package porter
 
 import (
+	"get.porter.sh/porter/pkg/cnab"
 	"get.porter.sh/porter/pkg/manifest"
 )
 
@@ -8,25 +9,28 @@ import (
 // based on values that beyond just what was supplied by the user
 // such as information in the manifest itself.
 func (p *Porter) applyDefaultOptions(opts *sharedOptions) error {
+	if opts.Name != "" {
+		return nil
+	}
+
 	if opts.File != "" {
-		err := p.LoadManifestFrom(opts.File)
+		m, err := manifest.LoadManifestFrom(p.Context, opts.File)
 		if err != nil {
 			return err
 		}
-	}
 
-	// Ensure that we have a manifest initialized, even if it's just an empty one
-	// This happens for non-porter bundles using --cnab-file or --reference
-	if p.Manifest == nil {
-		// TODO(carolynvs): change this to fix https://github.com/getporter/porter/issues/1024, we should hydrate a manifest from the bundle.json so that it is always available
-		p.Manifest = &manifest.Manifest{}
+		opts.Name = m.Name
+		return nil
 	}
 
-	//
-	// Default the installation name to the bundle name
-	//
-	if opts.Name == "" {
-		opts.Name = p.Manifest.Name
+	if opts.CNABFile != "" {
+		bun, err := cnab.LoadBundle(p.Context, opts.CNABFile)
+		if err != nil {
+			return err
+		}
+
+		opts.Name = bun.Name
+		return nil
 	}
 
 	return nil
diff --git a/pkg/porter/parameters.go b/pkg/porter/parameters.go
index d371a0fb..3ec54e54 100644
--- a/pkg/porter/parameters.go
+++ b/pkg/porter/parameters.go
@@ -326,7 +326,7 @@ func validateParameterName(args []string) error {
 }
 
 // loadParameterSets loads parameter values per their parameter set strategies
-func (p *Porter) loadParameterSets(namespace string, params []string) (secrets.Set, error) {
+func (p *Porter) loadParameterSets(bun cnab.ExtendedBundle, namespace string, params []string) (secrets.Set, error) {
 	resolvedParameters := secrets.Set{}
 	for _, name := range params {
 
@@ -351,10 +351,15 @@ func (p *Porter) loadParameterSets(namespace string, params []string) (secrets.S
 
 		// A parameter may correspond to a Porter-specific parameter type of 'file'
 		// If so, add value (filepath) directly to map and remove from pset
-		for _, paramDef := range p.Manifest.Parameters {
-			if paramDef.Type == "file" {
+		for paramName, paramDef := range bun.Parameters {
+			paramSchema, ok := bun.Definitions[paramDef.Definition]
+			if !ok {
+				return nil, fmt.Errorf("definition %s not defined in bundle", paramDef.Definition)
+			}
+
+			if bun.IsFileType(paramSchema) {
 				for i, param := range pset.Parameters {
-					if param.Name == paramDef.Name {
+					if param.Name == paramName {
 						// Pass through value (filepath) directly to resolvedParameters
 						resolvedParameters[param.Name] = param.Source.Value
 						// Eliminate this param from pset to prevent its resolution by
diff --git a/pkg/porter/porter.go b/pkg/porter/porter.go
index cfde7d1c..afbf22a4 100644
--- a/pkg/porter/porter.go
+++ b/pkg/porter/porter.go
@@ -14,7 +14,6 @@ import (
 	cnabprovider "get.porter.sh/porter/pkg/cnab/provider"
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/credentials"
-	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/mixin"
 	"get.porter.sh/porter/pkg/parameters"
 	"get.porter.sh/porter/pkg/plugins"
@@ -43,7 +42,6 @@ type Porter struct {
 	Claims      claims.Provider
 	Registry    cnabtooci.RegistryProvider
 	Templates   *templates.Templates
-	Manifest    *manifest.Manifest
 	Mixins      mixin.MixinProvider
 	Plugins     plugins.PluginProvider
 	CNAB        cnabprovider.CNABProvider
@@ -123,22 +121,6 @@ func (p *Porter) Close() error {
 	return bigErr.ErrorOrNil()
 }
 
-func (p *Porter) LoadManifest() error {
-	if p.Manifest != nil {
-		return nil
-	}
-	return p.LoadManifestFrom(config.Name)
-}
-
-func (p *Porter) LoadManifestFrom(file string) error {
-	m, err := manifest.LoadManifestFrom(p.Context, file)
-	if err != nil {
-		return err
-	}
-	p.Manifest = m
-	return nil
-}
-
 // NewBuilder creates a Builder based on the current configuration.
 func (p *Porter) GetBuilder() build.Builder {
 	if p.builder == nil {
diff --git a/pkg/porter/publish.go b/pkg/porter/publish.go
index 66665c0c..1b21a303 100644
--- a/pkg/porter/publish.go
+++ b/pkg/porter/publish.go
@@ -12,6 +12,7 @@ import (
 	"get.porter.sh/porter/pkg/build"
 	"get.porter.sh/porter/pkg/cnab"
 	portercontext "get.porter.sh/porter/pkg/context"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/tracing"
 	"github.com/cnabio/cnab-go/bundle/loader"
 	"github.com/cnabio/cnab-go/packager"
@@ -83,12 +84,6 @@ func (p *Porter) Publish(ctx context.Context, opts PublishOptions) error {
 	ctx, log := tracing.StartSpan(ctx)
 	defer log.EndSpan()
 
-	if opts.File != "" {
-		if err := p.LoadManifestFrom(opts.File); err != nil {
-			return err
-		}
-	}
-
 	if opts.ArchiveFile == "" {
 		return p.publishFromFile(ctx, opts)
 	}
@@ -113,64 +108,72 @@ func (p *Porter) publishFromFile(ctx context.Context, opts PublishOptions) error
 	if err != nil {
 		return err
 	}
+
+	var m *manifest.Manifest
 	if canonicalExists {
-		err := p.LoadManifestFrom(canonicalManifest)
+		m, err = manifest.LoadManifestFrom(p.Context, canonicalManifest)
 		if err != nil {
 			return err
 		}
+
 		// We still want the user-provided manifest path to be tracked,
 		// not Porter's canonical manifest path, for digest matching/auto-rebuilds
-		p.Manifest.ManifestPath = opts.File
+		m.ManifestPath = opts.File
+	} else {
+		m, err = manifest.LoadManifestFrom(p.Context, opts.File)
+		if err != nil {
+			return err
+		}
 	}
 
 	// Capture original invocation image name as it may be updated below
-	origInvImg := p.Manifest.Image
+	origInvImg := m.Image
 
 	// Check for tag and registry overrides optionally supplied on publish
 	if opts.Tag != "" {
-		p.Manifest.DockerTag = opts.Tag
+		m.DockerTag = opts.Tag
 	}
 	if opts.Registry != "" {
-		p.Manifest.Registry = opts.Registry
+		m.Registry = opts.Registry
 	}
 
 	// If either are non-empty, null out the reference on the manifest, as
 	// it needs to be rebuilt with new values
 	if opts.Tag != "" || opts.Registry != "" {
-		p.Manifest.Reference = ""
+		m.Reference = ""
 	}
 
 	// Update invocation image and reference with opts.Reference, which may be
 	// empty, which is fine - we still may need to pick up tag and/or registry
 	// overrides
-	if err := p.Manifest.SetInvocationImageAndReference(opts.Reference); err != nil {
+	if err := m.SetInvocationImageAndReference(opts.Reference); err != nil {
 		return errors.Wrap(err, "unable to set invocation image name and reference")
 	}
 
-	if origInvImg != p.Manifest.Image {
+	if origInvImg != m.Image {
 		// Tag it so that it will be known/found by Docker for publishing
 		builder := p.GetBuilder()
-		if err := builder.TagInvocationImage(ctx, origInvImg, p.Manifest.Image); err != nil {
+		if err := builder.TagInvocationImage(ctx, origInvImg, m.Image); err != nil {
 			return err
 		}
 	}
 
-	if p.Manifest.Reference == "" {
+	if m.Reference == "" {
 		return errors.New("porter.yaml is missing registry or reference values needed for publishing")
 	}
 
 	var bundleRef cnab.BundleReference
-	bundleRef.Reference, err = cnab.ParseOCIReference(p.Manifest.Reference)
+	bundleRef.Reference, err = cnab.ParseOCIReference(m.Reference)
 	if err != nil {
-		return errors.Wrapf(err, "invalid reference %s", p.Manifest.Reference)
+		return errors.Wrapf(err, "invalid reference %s", m.Reference)
 	}
 
-	bundleRef.Digest, err = p.Registry.PushInvocationImage(ctx, p.Manifest.Image)
+	bundleRef.Digest, err = p.Registry.PushInvocationImage(ctx, m.Image)
 	if err != nil {
-		return errors.Wrapf(err, "unable to push CNAB invocation image %q", p.Manifest.Image)
+		return errors.Wrapf(err, "unable to push CNAB invocation image %q", m.Image)
 	}
 
-	bundleRef.Definition, err = p.rewriteBundleWithInvocationImageDigest(bundleRef.Digest, opts.File)
+	bundleRef.Definition, err = p.rewriteBundleWithInvocationImageDigest(m, bundleRef.Digest)
 	if err != nil {
 		return err
 	}
@@ -375,14 +378,15 @@ func getNewImageNameFromBundleReference(origImg, bundleTag string) (image.Name,
 	return newImgName.WithTag(imgTag)
 }
 
-func (p *Porter) rewriteBundleWithInvocationImageDigest(digest digest.Digest, manifestPath string) (cnab.ExtendedBundle, error) {
-	taggedImage, err := p.rewriteImageWithDigest(p.Manifest.Image, digest.String())
+func (p *Porter) rewriteBundleWithInvocationImageDigest(m *manifest.Manifest, digest digest.Digest) (cnab.ExtendedBundle, error) {
+	taggedImage, err := p.rewriteImageWithDigest(m.Image, digest.String())
 	if err != nil {
 		return cnab.ExtendedBundle{}, errors.Wrap(err, "unable to update invocation image reference")
 	}
+	m.Image = taggedImage
 
 	fmt.Fprintln(p.Out, "\nRewriting CNAB bundle.json...")
-	err = p.buildBundle(taggedImage, digest)
+	err = p.buildBundle(m, digest)
 	if err != nil {
 		return cnab.ExtendedBundle{}, errors.Wrap(err, "unable to rewrite CNAB bundle.json with updated invocation image digest")
 	}
diff --git a/pkg/porter/reconcile.go b/pkg/porter/reconcile.go
index 7607a3f1..85c8573e 100644
--- a/pkg/porter/reconcile.go
+++ b/pkg/porter/reconcile.go
@@ -183,7 +183,7 @@ func (p *Porter) IsInstallationInSync(ctx context.Context, i claims.Installation
 	}
 
 	// Have the bundle parameters changed?
-	if err := opts.LoadParameters(p); err != nil {
+	if err := opts.LoadParameters(p, newRef.Definition); err != nil {
 		return false, err
 	}
 
diff --git a/pkg/porter/run.go b/pkg/porter/run.go
index a76fa668..a94c6b0d 100644
--- a/pkg/porter/run.go
+++ b/pkg/porter/run.go
@@ -5,6 +5,7 @@ import (
 	"strconv"
 
 	"get.porter.sh/porter/pkg/config"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/runtime"
 	"github.com/pkg/errors"
 )
@@ -72,12 +73,12 @@ func (o *RunOptions) defaultDebug() error {
 }
 
 func (p *Porter) Run(opts RunOptions) error {
-	err := p.LoadManifestFrom(opts.File)
+	m, err := manifest.LoadManifestFrom(p.Context, opts.File)
 	if err != nil {
 		return err
 	}
 
-	runtimeManifest := runtime.NewRuntimeManifest(p.Context, opts.Action, p.Manifest)
+	runtimeManifest := runtime.NewRuntimeManifest(p.Context, opts.Action, m)
 	r := runtime.NewPorterRuntime(p.Context, p.Mixins)
 	err = r.Execute(runtimeManifest)
 	if err == nil {
diff --git a/pkg/porter/stamp.go b/pkg/porter/stamp.go
index a6beafec..0cfb1780 100644
--- a/pkg/porter/stamp.go
+++ b/pkg/porter/stamp.go
@@ -8,6 +8,7 @@ import (
 	"get.porter.sh/porter/pkg/build"
 	"get.porter.sh/porter/pkg/cnab"
 	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/tracing"
 	"github.com/pkg/errors"
 )
@@ -55,7 +56,7 @@ func (p *Porter) IsBundleUpToDate(ctx context.Context, opts bundleFileOptions) (
 	if opts.File == "" {
 		return false, errors.New("File is required")
 	}
-	err := p.LoadManifestFrom(opts.File)
+	m, err := manifest.LoadManifestFrom(p.Context, opts.File)
 	if err != nil {
 		return false, err
 	}
@@ -92,12 +93,12 @@ func (p *Porter) IsBundleUpToDate(ctx context.Context, opts bundleFileOptions) (
 			return false, errors.Wrapf(err, "could not load stamp from %s", opts.CNABFile)
 		}
 
-		mixins, err := p.getUsedMixins()
+		mixins, err := p.getUsedMixins(m)
 		if err != nil {
 			return false, errors.Wrapf(err, "error while listing used mixins")
 		}
 
-		converter := configadapter.NewManifestConverter(p.Context, p.Manifest, nil, mixins)
+		converter := configadapter.NewManifestConverter(p.Context, m, nil, mixins)
 		newDigest, err := converter.DigestManifest()
 		if err != nil {
 			if p.Debug {


Test patch:
diff --git a/pkg/porter/build_integration_test.go b/pkg/porter/build_integration_test.go
index 96559c91..d5c8c5c1 100644
--- a/pkg/porter/build_integration_test.go
+++ b/pkg/porter/build_integration_test.go
@@ -14,6 +14,7 @@ import (
 	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/linter"
+	"get.porter.sh/porter/pkg/manifest"
 	"get.porter.sh/porter/pkg/mixin"
 	"get.porter.sh/porter/tests"
 	"github.com/cnabio/cnab-go/bundle"
@@ -35,9 +36,6 @@ func TestPorter_Build(t *testing.T) {
 	junkExists, _ := p.FileSystem.DirExists(junkDir)
 	assert.True(t, junkExists, "failed to create junk files for the test")
 
-	err = p.LoadManifestFrom(config.Name)
-	require.NoError(t, err)
-
 	opts := BuildOptions{}
 	require.NoError(t, opts.Validate(p.Porter), "Validate failed")
 
@@ -147,10 +145,10 @@ func TestPorter_paramRequired(t *testing.T) {
 
 	p.TestConfig.TestContext.AddTestFile("./testdata/paramafest.yaml", config.Name)
 
-	err := p.LoadManifestFrom(config.Name)
+	m, err := manifest.LoadManifestFrom(p.Context, config.Name)
 	require.NoError(t, err)
 
-	err = p.buildBundle("foo", "digest")
+	err = p.buildBundle(m, "digest")
 	require.NoError(t, err)
 
 	bundleBytes, err := p.FileSystem.ReadFile(build.LOCAL_BUNDLE)
diff --git a/pkg/porter/cnab_test.go b/pkg/porter/cnab_test.go
index 1712bd55..8e26edcc 100644
--- a/pkg/porter/cnab_test.go
+++ b/pkg/porter/cnab_test.go
@@ -5,8 +5,11 @@ import (
 
 	"get.porter.sh/porter/pkg"
 	"get.porter.sh/porter/pkg/build"
+	"get.porter.sh/porter/pkg/cnab"
+	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
 	"get.porter.sh/porter/pkg/config"
 	"get.porter.sh/porter/pkg/context"
+	"get.porter.sh/porter/pkg/manifest"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -120,7 +123,7 @@ func TestSharedOptions_ParseParamSets(t *testing.T) {
 	err := opts.Validate([]string{}, p.Porter)
 	assert.NoError(t, err)
 
-	err = opts.parseParamSets(p.Porter)
+	err = opts.parseParamSets(p.Porter, cnab.ExtendedBundle{})
 	assert.NoError(t, err)
 
 	wantParams := map[string]string{
@@ -133,9 +136,14 @@ func TestSharedOptions_ParseParamSets_Failed(t *testing.T) {
 	p := NewTestPorter(t)
 	defer p.Teardown()
 
-	p.TestConfig.TestContext.AddTestFile("testdata/porter-with-file-param.yaml", "porter.yaml")
+	p.TestConfig.TestContext.AddTestFile("testdata/porter-with-file-param.yaml", config.Name)
 	p.TestConfig.TestContext.AddTestFile("testdata/paramset-with-file-param.json", "/paramset.json")
 
+	m, err := manifest.LoadManifestFrom(p.Context, config.Name)
+	require.NoError(t, err)
+	bun, err := configadapter.ConvertToTestBundle(p.Context, m)
+	require.NoError(t, err)
+
 	opts := sharedOptions{
 		ParameterSets: []string{
 			"/paramset.json",
@@ -145,10 +153,10 @@ func TestSharedOptions_ParseParamSets_Failed(t *testing.T) {
 		},
 	}
 
-	err := opts.Validate([]string{}, p.Porter)
+	err = opts.Validate([]string{}, p.Porter)
 	assert.NoError(t, err)
 
-	err = opts.parseParamSets(p.Porter)
+	err = opts.parseParamSets(p.Porter, bun)
 	assert.Error(t, err)
 
 }
@@ -157,10 +165,16 @@ func TestSharedOptions_LoadParameters(t *testing.T) {
 	p := NewTestPorter(t)
 	defer p.Teardown()
 
+	p.TestConfig.TestContext.AddTestFile("testdata/porter.yaml", config.Name)
+	m, err := manifest.LoadManifestFrom(p.Context, config.Name)
+	require.NoError(t, err)
+	bun, err := configadapter.ConvertToTestBundle(p.Context, m)
+	require.NoError(t, err)
+
 	opts := sharedOptions{}
-	opts.Params = []string{"A=1", "B=2"}
+	opts.Params = []string{"my-first-param=1", "my-second-param=2"}
 
-	err := opts.LoadParameters(p.Porter)
+	err = opts.LoadParameters(p.Porter, bun)
 	require.NoError(t, err)
 
 	assert.Len(t, opts.Params, 2)
diff --git a/pkg/porter/lifecycle_integration_test.go b/pkg/porter/lifecycle_integration_test.go
index d03a5b1d..403fd137 100644
--- a/pkg/porter/lifecycle_integration_test.go
+++ b/pkg/porter/lifecycle_integration_test.go
@@ -31,7 +31,6 @@ func TestResolveBundleReference(t *testing.T) {
 		require.NoError(t, err)
 		require.NotEmpty(t, opts.Name)
 		require.NotEmpty(t, ref.Definition)
-		require.NotEmpty(t, p.Manifest)
 	})
 
 	t.Run("cnab file", func(t *testing.T) {
diff --git a/pkg/porter/packages_test.go b/pkg/porter/packages_test.go
index ebfb8d5d..bdfea439 100644
--- a/pkg/porter/packages_test.go
+++ b/pkg/porter/packages_test.go
@@ -51,16 +51,17 @@ func TestSearchOptions_Validate_PackageName(t *testing.T) {
 
 func TestPorter_SearchPackages_Mixins(t *testing.T) {
 	testcases := []struct {
-		name       string
-		mixin      string
-		format     printer.Format
-		wantOutput string
-		wantErr    string
+		name               string
+		mixin              string
+		format             printer.Format
+		wantOutput         string
+		wantNonEmptyOutput bool
+		wantErr            string
 	}{{
-		name:       "no name provided",
-		mixin:      "",
-		format:     printer.FormatJson,
-		wantOutput: "testdata/packages/search-no-query.txt",
+		name:               "no name provided",
+		mixin:              "",
+		format:             printer.FormatJson,
+		wantNonEmptyOutput: true,
 	}, {
 		name:       "mixin name single match",
 		mixin:      "az",
@@ -97,7 +98,13 @@ func TestPorter_SearchPackages_Mixins(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 				gotOutput := p.TestConfig.TestContext.GetOutput()
-				test.CompareGoldenFile(t, tc.wantOutput, gotOutput)
+
+				// Only check that the output isn't empty, but don't try to match the exact contents because it changes
+				if tc.wantNonEmptyOutput {
+					assert.NotEmpty(t, gotOutput, "expected the output to not be empty")
+				} else {
+					test.CompareGoldenFile(t, tc.wantOutput, gotOutput)
+				}
 			}
 		})
 	}
diff --git a/tests/integration/install_test.go b/tests/integration/install_test.go
index 903c0dd3..3e13042b 100644
--- a/tests/integration/install_test.go
+++ b/tests/integration/install_test.go
@@ -52,7 +52,7 @@ func TestInstall_fileParam(t *testing.T) {
 	p.SetupIntegrationTest()
 	p.Debug = false
 
-	p.AddTestBundleDir("testdata/bundles/bundle-with-file-params", false)
+	bundleName := p.AddTestBundleDir("testdata/bundles/bundle-with-file-params", false)
 
 	installOpts := porter.NewInstallOptions()
 	installOpts.Params = []string{"myfile=./myfile"}
@@ -76,7 +76,7 @@ func TestInstall_fileParam(t *testing.T) {
 	output := p.TestConfig.TestContext.GetOutput()
 	require.Contains(t, output, "Hello World!", "expected action output to contain provided file contents")
 
-	outputs, err := p.Claims.GetLastOutputs("", p.Manifest.Name)
+	outputs, err := p.Claims.GetLastOutputs("", bundleName)
 	require.NoError(t, err, "GetLastOutput failed")
 	myfile, ok := outputs.GetByName("myfile")
 	require.True(t, ok, "expected myfile output to be persisted")
diff --git a/tests/integration/invoke_test.go b/tests/integration/invoke_test.go
index 27979b22..4a26b22f 100644
--- a/tests/integration/invoke_test.go
+++ b/tests/integration/invoke_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package integration
@@ -24,7 +25,7 @@ func TestInvokeCustomAction(t *testing.T) {
 	err := p.Create()
 	require.NoError(t, err)
 
-	p.AddTestBundleDir("testdata/bundles/bundle-with-custom-action", true)
+	bundleName := p.AddTestBundleDir("testdata/bundles/bundle-with-custom-action", true)
 
 	installOpts := porter.NewInstallOptions()
 	err = installOpts.Validate([]string{}, p.Porter)
@@ -44,7 +45,7 @@ func TestInvokeCustomAction(t *testing.T) {
 	assert.Contains(t, gotOutput, "oh noes my brains", "invoke should have printed a cry for halp")
 
 	// Verify that the custom action was recorded properly
-	i, err := p.Claims.GetInstallation("", p.Manifest.Name)
+	i, err := p.Claims.GetInstallation("", bundleName)
 	require.NoError(t, err, "could not fetch installation")
 	c, err := p.Claims.GetLastRun(i.Namespace, i.Name)
 	require.NoError(t, err, "GetLastClaim failed")
diff --git a/tests/integration/outputs_test.go b/tests/integration/outputs_test.go
index d912d747..56f4dc57 100644
--- a/tests/integration/outputs_test.go
+++ b/tests/integration/outputs_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package integration
@@ -23,17 +24,17 @@ func TestExecOutputs(t *testing.T) {
 	p.SetupIntegrationTest()
 
 	// Install a bundle with exec outputs
-	installExecOutputsBundle(p)
+	bundleName := installExecOutputsBundle(p)
 	defer CleanupCurrentBundle(p)
 
 	// Verify that its file output was captured
-	usersOutput, err := p.ReadBundleOutput("users.json", p.Manifest.Name, "")
+	usersOutput, err := p.ReadBundleOutput("users.json", bundleName, "")
 	require.NoError(t, err, "could not read users output")
 	assert.Equal(t, fmt.Sprintln(`{"users": ["sally"]}`), usersOutput, "expected the users output to be populated correctly")
 
 	// Verify that its bundle level file output was captured
 	opts := porter.OutputListOptions{}
-	opts.Name = p.Manifest.Name
+	opts.Name = bundleName
 	opts.Format = printer.FormatPlaintext
 	displayOutputs, err := p.ListBundleOutputs(&opts)
 	require.NoError(t, err, "ListBundleOutputs failed")
@@ -52,19 +53,19 @@ func TestExecOutputs(t *testing.T) {
 	invokeExecOutputsBundle(p, "get-users")
 
 	// Verify logs were captured as an output
-	logs, err := p.ReadBundleOutput(cnab.OutputInvocationImageLogs, p.Manifest.Name, "")
+	logs, err := p.ReadBundleOutput(cnab.OutputInvocationImageLogs, bundleName, "")
 	require.NoError(t, err, "ListBundleOutputs failed")
 	assert.Contains(t, logs, "executing get-users action from exec-outputs", "expected the logs to contain bundle output from the last action")
 
 	// Verify that its jsonPath output was captured
-	userOutput, err := p.ReadBundleOutput("user-names", p.Manifest.Name, "")
+	userOutput, err := p.ReadBundleOutput("user-names", bundleName, "")
 	require.NoError(t, err, "could not read user-names output")
 	assert.Equal(t, `["sally","wei"]`, userOutput, "expected the user-names output to be populated correctly")
 
 	invokeExecOutputsBundle(p, "test")
 
 	// Verify that its regex output was captured
-	testOutputs, err := p.ReadBundleOutput("failed-tests", p.Manifest.Name, "")
+	testOutputs, err := p.ReadBundleOutput("failed-tests", bundleName, "")
 	require.NoError(t, err, "could not read failed-tests output")
 	assert.Equal(t, "TestInstall\nTestUpgrade", testOutputs, "expected the failed-tests output to be populated correctly")
 }
@@ -79,17 +80,19 @@ func CleanupCurrentBundle(p *porter.TestPorter) {
 	assert.NoError(p.T(), err, "uninstall failed for current bundle")
 }
 
-func installExecOutputsBundle(p *porter.TestPorter) {
+func installExecOutputsBundle(p *porter.TestPorter) string {
 	err := p.Create()
 	require.NoError(p.T(), err)
 
-	p.AddTestBundleDir(filepath.Join(p.RepoRoot, "examples/exec-outputs"), true)
+	bundleName := p.AddTestBundleDir(filepath.Join(p.RepoRoot, "examples/exec-outputs"), true)
 
 	installOpts := porter.NewInstallOptions()
 	err = installOpts.Validate([]string{}, p.Porter)
 	require.NoError(p.T(), err)
 	err = p.InstallBundle(context.Background(), installOpts)
 	require.NoError(p.T(), err)
+
+	return bundleName
 }
 
 func invokeExecOutputsBundle(p *porter.TestPorter, action string) {
diff --git a/tests/integration/rebuild_test.go b/tests/integration/rebuild_test.go
index a03853e2..8cc84bd8 100644
--- a/tests/integration/rebuild_test.go
+++ b/tests/integration/rebuild_test.go
@@ -127,10 +127,6 @@ func TestRebuild_GenerateCredentialsExistingBundle(t *testing.T) {
 	err = p.FileSystem.WriteFile(config.Name, data, pkg.FileModeWritable)
 	require.NoError(t, err)
 
-	// hack: simulate exactly what happens with the CLI where there is no persisted state between calls
-	// TODO: consider refactoring where we store manifest to better match the cli
-	p.Manifest = nil
-
 	// Re-generate the credentials
 	err = p.GenerateCredentials(context.Background(), credentialOptions)
 	require.NoError(t, err)
diff --git a/tests/integration/suppress_output_test.go b/tests/integration/suppress_output_test.go
index 47622bc2..e394442a 100644
--- a/tests/integration/suppress_output_test.go
+++ b/tests/integration/suppress_output_test.go
@@ -1,3 +1,4 @@
+//go:build integration
 // +build integration
 
 package integration
@@ -19,7 +20,7 @@ func TestSuppressOutput(t *testing.T) {
 	p.SetupIntegrationTest()
 	p.Debug = false
 
-	p.AddTestBundleDir("testdata/bundles/suppressed-output-example", true)
+	bundleName := p.AddTestBundleDir("testdata/bundles/suppressed-output-example", true)
 
 	// Install (Output suppressed)
 	installOpts := porter.NewInstallOptions()
@@ -30,7 +31,7 @@ func TestSuppressOutput(t *testing.T) {
 	require.NoError(t, err)
 
 	// Verify that the bundle output was captured (despite stdout/err of command being suppressed)
-	bundleOutput, err := p.ReadBundleOutput("greeting", p.Manifest.Name, "")
+	bundleOutput, err := p.ReadBundleOutput("greeting", bundleName, "")
 	require.NoError(t, err, "could not read config output")
 	require.Equal(t, "Hello World!", bundleOutput, "expected the bundle output to be populated correctly")
 
diff --git a/tests/testdata/config/config.toml b/tests/testdata/config/config.toml
index 1199d354..0a8eef78 100644
--- a/tests/testdata/config/config.toml
+++ b/tests/testdata/config/config.toml
@@ -1,10 +1,14 @@
 namespace = "dev"
-debug = false
-debug-plugins = false
+debug = true
+debug-plugins = true
 default-storage = "testdb"
-experimental = ["build-drivers"]
+experimental = ["build-drivers", "structured-logs"]
 build-driver = "buildkit"
 
+[logs]
+  enabled = true
+  level = "debug"
+
 [[storage]]
   name = "testdb"
   plugin = "mongodb"