Repository: getporter/porter
Pull request #2153 (2022-06-12 07:44:12)

Fix Vet Errors
# What does this change
This PR is meant to eliminate the go vet warnings/errors in the codebase.
Present State:
1 big commit which fixes the most common recurring vet errors (unkeyed fields in composite literals). 
There are 2 major culprits here:
`ExtendedBundle` which has an embedded `Bundle` struct. Relatively straightforward fix here to add `Bundle` before supplying the embedded struct. Even I didn't know this was a thing, always thought embedded structs had to be anonymous :) 
Found the solution from here: https://stackoverflow.com/questions/49461354/go-vet-composite-literal-uses-unkeyed-fields-with-embedded-types

The other one is in the Primitive.E type in the mongo driver. We are generally passing Key and Value as a `{"some_key", "some_value"}` in all places. The recommended way is to go with `{Key: "some_key", Value: "some_value"}`. This feels a little extra verbose to me, but as highlighted in the link below, it offers good protection against changes in the library, so I suggest we keep it. Also fixes vet warnings :)
https://stackoverflow.com/questions/54548441/composite-literal-uses-unkeyed-fields

Update:
I have added another commit to resolve all the Struct Tag issues. Only minor typos here (although, it's surprising functionality wasn't broken in a few places). Nevertheless, all resolved now.

Update 2:
I have also renamed the example tests to use suffixes according to (https://pkg.go.dev/testing#hdr-Examples).
The old values were incompatible with Vet.

# What issue does it fix
Makes incremental progress on  #2081 

# Notes for the reviewer
This is a major chunk of the go vet warnings, but not all. I will resolve the rest via a separate PR. I kept this one segregated because of the theme.
Update: Also added a commit to resolve struct tag issues.

# 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

tchaudhry91: I have added a comment regarding the broken unit-test. The malformed struct tag for yaml meant the file wasn't using the correct name. Will defer to someone with more knowledge to get the best way to fix this. Long term, we probably want to keep the correct tag and regenerate the golden comparison file. Not sure how much that will impact current consumers. Alternatively, we could use the "wrong" value in the tag if we want. Let me know.
VinozzZ: Thank you for submitting the PR to fix lint and vet errors!

> I have added a comment regarding the broken unit-test. The malformed struct tag for yaml meant the file wasn't using the correct name. Will defer to someone with more knowledge to get the best way to fix this. Long term, we probably want to keep the correct tag and regenerate the golden comparison file. Not sure how much that will impact current consumers. Alternatively, we could use the "wrong" value in the tag if we want. Let me know.

Thank you for putting up the PR for fixing lint and vet errors.  From my perspective,, we should fix all the schema issues while we are still in alpha. but we probably should wait for  @carolynvs to confirm it
tchaudhry91: Hey all,
This PR is now complete from my point of view, here is a summary for the reviewers:
1. Fixes all the unkeyed fields in the composite literals.
2. Fixes the struct tags. This is a bit more complicated as this will technically be a breaking change? This is because the old tags were incorrect and hence ignored. So far, this has been detected in one place in a test file, but I assume others would also be impacted. For now, I have regenerated that test file and the tests pass now.
3. Renamed ExampleTests to use PackageName_suffix format as dictated by Vet.

The unit test failure in the PR seems to be due to a connection reset, i.e flaky. Can someone re-run it? I have confirmed that unit tests run fine on multiple machines.

carolynvs: I restarted the test run, it was a temporary glitch in the matrix. 😀 

Patch:
diff --git a/cmd/porter/credentials.go b/cmd/porter/credentials.go
index 31c934c9..30d87841 100644
--- a/cmd/porter/credentials.go
+++ b/cmd/porter/credentials.go
@@ -143,7 +143,8 @@ The results may also be filtered by associated labels and the namespace in which
   porter credentials list --namespace prod
   porter credentials list --all-namespaces,
   porter credentials list --name myapp
-  porter credentials list --label env=dev`,
+  porter credentials list --label env=dev
+  porter credentials list --skip 2 --limit 2`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return opts.Validate()
 		},
@@ -163,6 +164,10 @@ The results may also be filtered by associated labels and the namespace in which
 		"Filter the credential sets by a label formatted as: KEY=VALUE. May be specified multiple times.")
 	f.StringVarP(&opts.RawFormat, "output", "o", "plaintext",
 		"Specify an output format.  Allowed values: plaintext, json, yaml")
+	f.Int64Var(&opts.Skip, "skip", 0,
+		"Skip the number of credential sets by a certain amount. Defaults to 0.")
+	f.Int64Var(&opts.Limit, "limit", 0,
+		"Limit the number of credential sets by a certain amount. Defaults to 0.")
 
 	return cmd
 }
diff --git a/cmd/porter/installations.go b/cmd/porter/installations.go
index b78b16ab..04abb8b4 100644
--- a/cmd/porter/installations.go
+++ b/cmd/porter/installations.go
@@ -44,7 +44,8 @@ Optional output formats include json and yaml.`,
   porter installations list -o json
   porter installations list --all-namespaces,
   porter installations list --label owner=myname --namespace dev
-  porter installations list --name myapp`,
+  porter installations list --name myapp
+  porter installations list --skip 2 --limit 2`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return opts.Validate()
 		},
@@ -64,6 +65,10 @@ Optional output formats include json and yaml.`,
 		"Filter the installations by a label formatted as: KEY=VALUE. May be specified multiple times.")
 	f.StringVarP(&opts.RawFormat, "output", "o", "plaintext",
 		"Specify an output format.  Allowed values: plaintext, json, yaml")
+	f.Int64Var(&opts.Skip, "skip", 0,
+		"Skip the number of installations by a certain amount. Defaults to 0.")
+	f.Int64Var(&opts.Limit, "limit", 0,
+		"Limit the number of installations by a certain amount. Defaults to 0.")
 
 	return cmd
 }
diff --git a/cmd/porter/parameters.go b/cmd/porter/parameters.go
index ef004dd4..ea00b938 100644
--- a/cmd/porter/parameters.go
+++ b/cmd/porter/parameters.go
@@ -142,7 +142,8 @@ The results may also be filtered by associated labels and the namespace in which
   porter parameters list --namespace prod -o json
   porter parameters list --all-namespaces,
   porter parameters list --name myapp
-  porter parameters list --label env=dev`,
+  porter parameters list --label env=dev
+  porter parameters list --skip 2 --limit 2`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return opts.Validate()
 		},
@@ -162,6 +163,10 @@ The results may also be filtered by associated labels and the namespace in which
 		"Filter the parameter sets by a label formatted as: KEY=VALUE. May be specified multiple times.")
 	f.StringVarP(&opts.RawFormat, "output", "o", "plaintext",
 		"Specify an output format.  Allowed values: plaintext, json, yaml")
+	f.Int64Var(&opts.Skip, "skip", 0,
+		"Skip the number of parameter sets by a certain amount. Defaults to 0.")
+	f.Int64Var(&opts.Limit, "limit", 0,
+		"Limit the number of parameter sets by a certain amount. Defaults to 0.")
 
 	return cmd
 }
diff --git a/docs/content/cli/build.md b/docs/content/cli/build.md
index 83f25cd2..155de217 100644
--- a/docs/content/cli/build.md
+++ b/docs/content/cli/build.md
@@ -39,8 +39,8 @@ porter build [flags]
 ```
       --build-arg stringArray   Set build arguments in the template Dockerfile (format: NAME=VALUE). May be specified multiple times.
       --custom stringArray      Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times.
-  -d, --dir string              Path to the build context directory where all bundle assets are located.
-  -f, --file porter.yaml        Path to the Porter manifest. Defaults to porter.yaml in the current directory.
+  -d, --dir string              Path to the build context directory where all bundle assets are located. Defaults to the current directory.
+  -f, --file string             Path to the Porter manifest. The path is relative to the build context directory. Defaults to porter.yaml in the current directory.
   -h, --help                    help for build
       --name string             Override the bundle name
       --no-cache                Do not use the Docker cache when building the bundle's invocation image.
diff --git a/docs/content/cli/bundles_build.md b/docs/content/cli/bundles_build.md
index 9f7a0f1b..6498ac0d 100644
--- a/docs/content/cli/bundles_build.md
+++ b/docs/content/cli/bundles_build.md
@@ -39,8 +39,8 @@ porter bundles build [flags]
 ```
       --build-arg stringArray   Set build arguments in the template Dockerfile (format: NAME=VALUE). May be specified multiple times.
       --custom stringArray      Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times.
-  -d, --dir string              Path to the build context directory where all bundle assets are located.
-  -f, --file porter.yaml        Path to the Porter manifest. Defaults to porter.yaml in the current directory.
+  -d, --dir string              Path to the build context directory where all bundle assets are located. Defaults to the current directory.
+  -f, --file string             Path to the Porter manifest. The path is relative to the build context directory. Defaults to porter.yaml in the current directory.
   -h, --help                    help for build
       --name string             Override the bundle name
       --no-cache                Do not use the Docker cache when building the bundle's invocation image.
diff --git a/docs/content/cli/credentials_list.md b/docs/content/cli/credentials_list.md
index 5a5940d5..d0ec4fef 100644
--- a/docs/content/cli/credentials_list.md
+++ b/docs/content/cli/credentials_list.md
@@ -26,6 +26,7 @@ porter credentials list [flags]
   porter credentials list --all-namespaces,
   porter credentials list --name myapp
   porter credentials list --label env=dev
+  porter credentials list --skip 2 --limit 2
 ```
 
 ### Options
@@ -34,9 +35,11 @@ porter credentials list [flags]
       --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the credential sets by a label formatted as: KEY=VALUE. May be specified multiple times.
+      --limit int          Limit the number of credential sets by a certain amount. Defaults to 0.
       --name string        Filter the credential sets where the name contains the specified substring.
   -n, --namespace string   Namespace in which the credential set is defined. Defaults to the global namespace. Use * to list across all namespaces.
   -o, --output string      Specify an output format.  Allowed values: plaintext, json, yaml (default "plaintext")
+      --skip int           Skip the number of credential sets by a certain amount. Defaults to 0.
 ```
 
 ### Options inherited from parent commands
diff --git a/docs/content/cli/installations_list.md b/docs/content/cli/installations_list.md
index 33e68c36..081fdf28 100644
--- a/docs/content/cli/installations_list.md
+++ b/docs/content/cli/installations_list.md
@@ -29,6 +29,7 @@ porter installations list [flags]
   porter installations list --all-namespaces,
   porter installations list --label owner=myname --namespace dev
   porter installations list --name myapp
+  porter installations list --skip 2 --limit 2
 ```
 
 ### Options
@@ -37,9 +38,11 @@ porter installations list [flags]
       --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the installations by a label formatted as: KEY=VALUE. May be specified multiple times.
+      --limit int          Limit the number of installations by a certain amount. Defaults to 0.
       --name string        Filter the installations where the name contains the specified substring.
   -n, --namespace string   Filter the installations by namespace. Defaults to the global namespace.
   -o, --output string      Specify an output format.  Allowed values: plaintext, json, yaml (default "plaintext")
+      --skip int           Skip the number of installations by a certain amount. Defaults to 0.
 ```
 
 ### Options inherited from parent commands
diff --git a/docs/content/cli/list.md b/docs/content/cli/list.md
index b3388da3..d7a44d0c 100644
--- a/docs/content/cli/list.md
+++ b/docs/content/cli/list.md
@@ -29,6 +29,7 @@ porter list [flags]
   porter list --all-namespaces,
   porter list --label owner=myname --namespace dev
   porter list --name myapp
+  porter list --skip 2 --limit 2
 ```
 
 ### Options
@@ -37,9 +38,11 @@ porter list [flags]
       --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the installations by a label formatted as: KEY=VALUE. May be specified multiple times.
+      --limit int          Limit the number of installations by a certain amount. Defaults to 0.
       --name string        Filter the installations where the name contains the specified substring.
   -n, --namespace string   Filter the installations by namespace. Defaults to the global namespace.
   -o, --output string      Specify an output format.  Allowed values: plaintext, json, yaml (default "plaintext")
+      --skip int           Skip the number of installations by a certain amount. Defaults to 0.
 ```
 
 ### Options inherited from parent commands
diff --git a/docs/content/cli/parameters_list.md b/docs/content/cli/parameters_list.md
index 5787a42c..a6ade63e 100644
--- a/docs/content/cli/parameters_list.md
+++ b/docs/content/cli/parameters_list.md
@@ -26,6 +26,7 @@ porter parameters list [flags]
   porter parameters list --all-namespaces,
   porter parameters list --name myapp
   porter parameters list --label env=dev
+  porter parameters list --skip 2 --limit 2
 ```
 
 ### Options
@@ -34,9 +35,11 @@ porter parameters list [flags]
       --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the parameter sets by a label formatted as: KEY=VALUE. May be specified multiple times.
+      --limit int          Limit the number of parameter sets by a certain amount. Defaults to 0.
       --name string        Filter the parameter sets where the name contains the specified substring.
   -n, --namespace string   Namespace in which the parameter set is defined. Defaults to the global namespace. Use * to list across all namespaces.
   -o, --output string      Specify an output format.  Allowed values: plaintext, json, yaml (default "plaintext")
+      --skip int           Skip the number of parameter sets by a certain amount. Defaults to 0.
 ```
 
 ### Options inherited from parent commands
diff --git a/go.sum b/go.sum
index da0ea075..3103dce3 100644
--- a/go.sum
+++ b/go.sum
@@ -53,8 +53,6 @@ contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e
 contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
 contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-get.porter.sh/magefiles v0.1.3 h1:91Y7vFDHGmMBbRfHQqEcIlqpp/RfDCMhyHGVusTYlmE=
-get.porter.sh/magefiles v0.1.3/go.mod h1:Whw/DSX8dcgn7dUPb6csbY6PtuTy1wujwFR2G6ONf20=
 get.porter.sh/magefiles v0.3.0 h1:uwCOTblBx2RFN2IEgIUDP4sNj/C4F26KqAdqSREJL7g=
 get.porter.sh/magefiles v0.3.0/go.mod h1:lwDECEEivbBHACLImnDEhwlr8g3E4xZR1W0DO8FOGa8=
 git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
diff --git a/pkg/cli/config.go b/pkg/cli/config.go
index 2f7ca332..f7ce9141 100644
--- a/pkg/cli/config.go
+++ b/pkg/cli/config.go
@@ -71,6 +71,8 @@ func getViperValue(flags *pflag.FlagSet, f *pflag.Flag) interface{} {
 	switch flagType {
 	case "int":
 		out, err = flags.GetInt(f.Name)
+	case "int64":
+		out, err = flags.GetInt64(f.Name)
 	case "string":
 		out, err = flags.GetString(f.Name)
 	case "bool":
diff --git a/pkg/cnab/cnab-to-oci/registry.go b/pkg/cnab/cnab-to-oci/registry.go
index 2d77ff89..2999124a 100644
--- a/pkg/cnab/cnab-to-oci/registry.go
+++ b/pkg/cnab/cnab-to-oci/registry.go
@@ -76,7 +76,7 @@ func (r *Registry) PullBundle(ref cnab.OCIReference, insecureRegistry bool) (cna
 	bundleRef := cnab.BundleReference{
 		Reference:     ref,
 		Digest:        digest,
-		Definition:    cnab.ExtendedBundle{*bun},
+		Definition:    cnab.NewBundle(*bun),
 		RelocationMap: reloMap,
 	}
 
diff --git a/pkg/cnab/config-adapter/adapter.go b/pkg/cnab/config-adapter/adapter.go
index 32c28122..98f6f3e8 100644
--- a/pkg/cnab/config-adapter/adapter.go
+++ b/pkg/cnab/config-adapter/adapter.go
@@ -48,14 +48,14 @@ func (c *ManifestConverter) ToBundle(ctx context.Context) (cnab.ExtendedBundle,
 		return cnab.ExtendedBundle{}, span.Error(err)
 	}
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		SchemaVersion: SchemaVersion,
 		Name:          c.Manifest.Name,
 		Description:   c.Manifest.Description,
 		Version:       c.Manifest.Version,
 		Maintainers:   c.generateBundleMaintainers(),
 		Custom:        make(map[string]interface{}, 1),
-	}}
+	})
 	image := bundle.InvocationImage{
 		BaseImage: bundle.BaseImage{
 			Image:     c.Manifest.Image,
diff --git a/pkg/cnab/extended_bundle.go b/pkg/cnab/extended_bundle.go
index 16f45de7..04d3a13b 100644
--- a/pkg/cnab/extended_bundle.go
+++ b/pkg/cnab/extended_bundle.go
@@ -17,6 +17,11 @@ type ExtendedBundle struct {
 	bundle.Bundle
 }
 
+// NewBundle creates an ExtendedBundle from a given bundle.
+func NewBundle(bundle bundle.Bundle) ExtendedBundle {
+	return ExtendedBundle{bundle}
+}
+
 // LoadBundle from the specified filepath.
 func LoadBundle(c *portercontext.Context, bundleFile string) (ExtendedBundle, error) {
 	bunD, err := c.FileSystem.ReadFile(bundleFile)
@@ -29,7 +34,7 @@ func LoadBundle(c *portercontext.Context, bundleFile string) (ExtendedBundle, er
 		return ExtendedBundle{}, errors.Wrapf(err, "cannot load bundle from\n%s at %s", string(bunD), bundleFile)
 	}
 
-	return ExtendedBundle{*bun}, nil
+	return NewBundle(*bun), nil
 }
 
 // IsPorterBundle determines if the bundle was created by Porter.
diff --git a/pkg/cnab/helpers.go b/pkg/cnab/helpers.go
index 020cdbd5..5c330351 100644
--- a/pkg/cnab/helpers.go
+++ b/pkg/cnab/helpers.go
@@ -15,5 +15,5 @@ func ReadTestBundle(t *testing.T, path string) ExtendedBundle {
 	bun, err := bundle.Unmarshal(bunD)
 	require.NoError(t, err, "Unmarshal failed for bundle at %s", path)
 
-	return ExtendedBundle{*bun}
+	return NewBundle(*bun)
 }
diff --git a/pkg/cnab/provider/action.go b/pkg/cnab/provider/action.go
index ddf4c017..d0f257f7 100644
--- a/pkg/cnab/provider/action.go
+++ b/pkg/cnab/provider/action.go
@@ -198,7 +198,7 @@ func (r *Runtime) CreateRun(ctx context.Context, args ActionArguments, b cnab.Ex
 	currentRun.BundleDigest = args.BundleReference.Digest.String()
 
 	var err error
-	extb := cnab.ExtendedBundle{b.Bundle}
+	extb := cnab.NewBundle(b.Bundle)
 	currentRun.Parameters.Parameters, err = r.sanitizer.CleanRawParameters(ctx, args.Params, extb, currentRun.ID)
 	if err != nil {
 		return storage.Run{}, span.Error(err)
@@ -271,7 +271,7 @@ func (r *Runtime) SaveOperationResult(ctx context.Context, opResult driver.Opera
 
 	for outputName, outputValue := range opResult.Outputs {
 		output := result.NewOutput(outputName, []byte(outputValue))
-		output, err = r.sanitizer.CleanOutput(ctx, output, cnab.ExtendedBundle{run.Bundle})
+		output, err = r.sanitizer.CleanOutput(ctx, output, cnab.ExtendedBundle{Bundle: run.Bundle})
 		if err != nil {
 			bigerr = multierror.Append(bigerr, errors.Wrapf(err, "error sanitizing sensitive %s output for %s run of installation %s\n%#v", output.Name, run.Action, installation, output))
 		}
diff --git a/pkg/porter/archive.go b/pkg/porter/archive.go
index ddaccd12..35d64653 100644
--- a/pkg/porter/archive.go
+++ b/pkg/porter/archive.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	"get.porter.sh/porter/pkg"
@@ -16,6 +17,7 @@ import (
 	"github.com/cnabio/cnab-go/imagestore/construction"
 	"github.com/docker/docker/pkg/archive"
 	"github.com/pkg/errors"
+	"github.com/spf13/afero"
 )
 
 // ArchiveOptions defines the valid options for performing an archive operation
@@ -88,14 +90,10 @@ type exporter struct {
 }
 
 func (ex *exporter) export() error {
-
 	name := ex.bundle.Name + "-" + ex.bundle.Version
-	archiveDir, err := ex.fs.TempDir("", name)
+	archiveDir, err := ex.createArchiveFolder(name)
 	if err != nil {
-		return err
-	}
-	if err := ex.fs.MkdirAll(archiveDir, 0644); err != nil {
-		return err
+		return fmt.Errorf("can not create archive folder: %w", err)
 	}
 	defer ex.fs.RemoveAll(archiveDir)
 
@@ -186,6 +184,23 @@ func (ex *exporter) addImage(image bundle.BaseImage) error {
 	return checkDigest(image, dig)
 }
 
+// createArchiveFolder set up a temporary directory for storing all data needed to archive a bundle.
+// It sanitizes the name and make sure only the current user has full permission to it.
+// If the name contains a path separator, all path separators will be replaced with "-".
+func (ex *exporter) createArchiveFolder(name string) (string, error) {
+	cleanedPath := strings.ReplaceAll(afero.UnicodeSanitize(name), string(os.PathSeparator), "-")
+	archiveDir, err := ex.fs.TempDir("", cleanedPath)
+	if err != nil {
+		return "", fmt.Errorf("can not create a temporary archive folder: %w", err)
+	}
+
+	err = ex.fs.Chmod(archiveDir, pkg.FileModeDirectory)
+	if err != nil {
+		return "", fmt.Errorf("can not change permission for the temporary archive folder: %w", err)
+	}
+	return archiveDir, nil
+}
+
 // checkDigest compares the content digest of the given image to the given content digest and returns an error if they
 // are both non-empty and do not match
 func checkDigest(image bundle.BaseImage, dig string) error {
diff --git a/pkg/porter/credentials.go b/pkg/porter/credentials.go
index f30b1365..05e83720 100644
--- a/pkg/porter/credentials.go
+++ b/pkg/porter/credentials.go
@@ -31,7 +31,13 @@ type CredentialEditOptions struct {
 
 // ListCredentials lists saved credential sets.
 func (p *Porter) ListCredentials(ctx context.Context, opts ListOptions) ([]storage.CredentialSet, error) {
-	return p.Credentials.ListCredentialSets(ctx, opts.GetNamespace(), opts.Name, opts.ParseLabels())
+	return p.Credentials.ListCredentialSets(ctx, storage.ListOptions{
+		Namespace: opts.GetNamespace(),
+		Name:      opts.Name,
+		Labels:    opts.ParseLabels(),
+		Skip:      opts.Skip,
+		Limit:     opts.Limit,
+	})
 }
 
 // PrintCredentials prints saved credential sets.
diff --git a/pkg/porter/dependencies.go b/pkg/porter/dependencies.go
index 28a58022..abf5682b 100644
--- a/pkg/porter/dependencies.go
+++ b/pkg/porter/dependencies.go
@@ -141,7 +141,7 @@ func (e *dependencyExecutioner) identifyDependencies(ctx context.Context) error
 			return err
 		}
 
-		bun = cnab.ExtendedBundle{c.Bundle}
+		bun = cnab.NewBundle(c.Bundle)
 	} else {
 		// If we hit here, there is a bug somewhere
 		return errors.New("identifyDependencies failed to load the bundle because no bundle was specified. Please report this bug to https://github.com/getporter/porter/issues/new/choose")
diff --git a/pkg/porter/helpers.go b/pkg/porter/helpers.go
index d2bc539e..f97cb442 100644
--- a/pkg/porter/helpers.go
+++ b/pkg/porter/helpers.go
@@ -182,7 +182,7 @@ func (p *TestPorter) ReadBundle(path string) cnab.ExtendedBundle {
 	bun, err := bundle.Unmarshal(bunD)
 	require.NoError(p.T(), err, "Unmarshal failed for bundle at %s", path)
 
-	return cnab.ExtendedBundle{*bun}
+	return cnab.NewBundle(*bun)
 }
 
 func (p *TestPorter) RandomString(len int) string {
diff --git a/pkg/porter/lifecycle.go b/pkg/porter/lifecycle.go
index edfbfde5..7a61816f 100644
--- a/pkg/porter/lifecycle.go
+++ b/pkg/porter/lifecycle.go
@@ -140,7 +140,7 @@ func (p *Porter) resolveBundleReference(ctx context.Context, opts *BundleActionO
 			}
 
 			bundleRef = cnab.BundleReference{
-				Definition: cnab.ExtendedBundle{lastRun.Bundle},
+				Definition: cnab.NewBundle(lastRun.Bundle),
 				Digest:     digest.Digest(lastRun.BundleDigest)}
 
 			if lastRun.BundleReference != "" {
diff --git a/pkg/porter/list.go b/pkg/porter/list.go
index a8aeb876..743c685b 100644
--- a/pkg/porter/list.go
+++ b/pkg/porter/list.go
@@ -24,6 +24,8 @@ type ListOptions struct {
 	Namespace     string
 	Name          string
 	Labels        []string
+	Skip          int64
+	Limit         int64
 }
 
 func (o *ListOptions) Validate() error {
@@ -103,7 +105,7 @@ type DisplayInstallation struct {
 }
 
 type DisplayInstallationMetadata struct {
-	ResolvedParameters DisplayValues `json:"resolvedParameters", yaml:"resolvedParameters"`
+	ResolvedParameters DisplayValues `json:"resolvedParameters" yaml:"resolvedParameters"`
 }
 
 func NewDisplayInstallation(installation storage.Installation) DisplayInstallation {
@@ -218,7 +220,13 @@ func (p *Porter) ListInstallations(ctx context.Context, opts ListOptions) (Displ
 	ctx, log := tracing.StartSpan(ctx)
 	defer log.EndSpan()
 
-	installations, err := p.Installations.ListInstallations(ctx, opts.GetNamespace(), opts.Name, opts.ParseLabels())
+	installations, err := p.Installations.ListInstallations(ctx, storage.ListOptions{
+		Namespace: opts.GetNamespace(),
+		Name:      opts.Name,
+		Labels:    opts.ParseLabels(),
+		Skip:      opts.Skip,
+		Limit:     opts.Limit,
+	})
 	if err != nil {
 		return nil, log.Error(fmt.Errorf("could not list installations: %w", err))
 	}
diff --git a/pkg/porter/outputs.go b/pkg/porter/outputs.go
index 94a85281..1570748f 100644
--- a/pkg/porter/outputs.go
+++ b/pkg/porter/outputs.go
@@ -135,7 +135,7 @@ func (p *Porter) ListBundleOutputs(ctx context.Context, opts *OutputListOptions)
 		return nil, err
 	}
 
-	bun := cnab.ExtendedBundle{c.Bundle}
+	bun := cnab.NewBundle(c.Bundle)
 
 	displayOutputs := NewDisplayValuesFromOutputs(bun, resolved)
 	if err != nil {
diff --git a/pkg/porter/parameters.go b/pkg/porter/parameters.go
index 2dc9467d..218944f6 100644
--- a/pkg/porter/parameters.go
+++ b/pkg/porter/parameters.go
@@ -38,7 +38,13 @@ type ParameterEditOptions struct {
 
 // ListParameters lists saved parameter sets.
 func (p *Porter) ListParameters(ctx context.Context, opts ListOptions) ([]storage.ParameterSet, error) {
-	return p.Parameters.ListParameterSets(ctx, opts.GetNamespace(), opts.Name, opts.ParseLabels())
+	return p.Parameters.ListParameterSets(ctx, storage.ListOptions{
+		Namespace: opts.GetNamespace(),
+		Name:      opts.Name,
+		Labels:    opts.ParseLabels(),
+		Skip:      opts.Skip,
+		Limit:     opts.Limit,
+	})
 }
 
 // PrintParameters prints saved parameter sets.
diff --git a/pkg/porter/publish.go b/pkg/porter/publish.go
index 0e53e698..8f9cf7da 100644
--- a/pkg/porter/publish.go
+++ b/pkg/porter/publish.go
@@ -296,7 +296,7 @@ func (p *Porter) extractBundle(tmpDir, source string) (cnab.ExtendedBundle, erro
 		return cnab.ExtendedBundle{}, errors.Wrapf(err, "failed to load bundle from archive %s", source)
 	}
 
-	return cnab.ExtendedBundle{*bun}, nil
+	return cnab.NewBundle(*bun), nil
 }
 
 // pushUpdatedImage uses the provided layout to find the provided origImg,
diff --git a/pkg/porter/reconcile.go b/pkg/porter/reconcile.go
index 39856ac8..8c0883ec 100644
--- a/pkg/porter/reconcile.go
+++ b/pkg/porter/reconcile.go
@@ -221,7 +221,7 @@ func (p *Porter) IsInstallationInSync(ctx context.Context, i storage.Installatio
 		return compParams, nil
 	}
 
-	lastRunParams, err := p.Sanitizer.RestoreParameterSet(ctx, lastRun.Parameters, cnab.ExtendedBundle{lastRun.Bundle})
+	lastRunParams, err := p.Sanitizer.RestoreParameterSet(ctx, lastRun.Parameters, cnab.NewBundle(lastRun.Bundle))
 	if err != nil {
 		return false, err
 	}
diff --git a/pkg/porter/show.go b/pkg/porter/show.go
index b529e460..e39c2e6f 100644
--- a/pkg/porter/show.go
+++ b/pkg/porter/show.go
@@ -175,7 +175,7 @@ func (p *Porter) NewDisplayInstallationWithSecrets(ctx context.Context, installa
 	displayInstallation := NewDisplayInstallation(installation)
 
 	if run != nil {
-		bun := cnab.ExtendedBundle{run.Bundle}
+		bun := cnab.NewBundle(run.Bundle)
 		installParams, err := p.Sanitizer.RestoreParameterSet(ctx, installation.Parameters, bun)
 		if err != nil {
 			return DisplayInstallation{}, err
diff --git a/pkg/runtime/runtime-manifest.go b/pkg/runtime/runtime-manifest.go
index 2bd8cb40..c99b73d7 100644
--- a/pkg/runtime/runtime-manifest.go
+++ b/pkg/runtime/runtime-manifest.go
@@ -109,7 +109,7 @@ func (m *RuntimeManifest) loadDependencyDefinitions() error {
 			return errors.Wrapf(err, "error unmarshaling bundle definition for dependency %s", dep.Name)
 		}
 
-		m.bundles[dep.Name] = cnab.ExtendedBundle{*bun}
+		m.bundles[dep.Name] = cnab.NewBundle(*bun)
 	}
 
 	return nil
diff --git a/pkg/storage/credential_store.go b/pkg/storage/credential_store.go
index 71acd600..e30ee6d5 100644
--- a/pkg/storage/credential_store.go
+++ b/pkg/storage/credential_store.go
@@ -110,12 +110,9 @@ func (s CredentialStore) InsertCredentialSet(ctx context.Context, creds Credenti
 	return s.Documents.Insert(ctx, CollectionCredentials, opts)
 }
 
-func (s CredentialStore) ListCredentialSets(ctx context.Context, namespace string, name string, labels map[string]string) ([]CredentialSet, error) {
+func (s CredentialStore) ListCredentialSets(ctx context.Context, listOptions ListOptions) ([]CredentialSet, error) {
 	var out []CredentialSet
-	opts := FindOptions{
-		Filter: CreateListFiler(namespace, name, labels),
-	}
-	err := s.Documents.Find(ctx, CollectionCredentials, opts, &out)
+	err := s.Documents.Find(ctx, CollectionCredentials, listOptions.ToFindOptions(), &out)
 	return out, err
 }
 
diff --git a/pkg/storage/credentialset_provider.go b/pkg/storage/credentialset_provider.go
index 8db1faa0..cfa83eb3 100644
--- a/pkg/storage/credentialset_provider.go
+++ b/pkg/storage/credentialset_provider.go
@@ -12,7 +12,7 @@ type CredentialSetProvider interface {
 	ResolveAll(ctx context.Context, creds CredentialSet) (secrets.Set, error)
 	Validate(ctx context.Context, creds CredentialSet) error
 	InsertCredentialSet(ctx context.Context, creds CredentialSet) error
-	ListCredentialSets(ctx context.Context, namespace string, name string, labels map[string]string) ([]CredentialSet, error)
+	ListCredentialSets(ctx context.Context, listOptions ListOptions) ([]CredentialSet, error)
 	GetCredentialSet(ctx context.Context, namespace string, name string) (CredentialSet, error)
 	UpdateCredentialSet(ctx context.Context, creds CredentialSet) error
 	RemoveCredentialSet(ctx context.Context, namespace string, name string) error
diff --git a/pkg/storage/installation.go b/pkg/storage/installation.go
index 2997e68b..20a7ea5d 100644
--- a/pkg/storage/installation.go
+++ b/pkg/storage/installation.go
@@ -200,7 +200,7 @@ type InstallationStatus struct {
 
 	// Uninstalled indicates if the installation has successfully completed the uninstall action.
 	// Once that state is reached, Porter should not allow further stateful actions.
-	Uninstalled *time.Time `json:"uninstalled" yaml"uninstalled" toml:"uninstalled"`
+	Uninstalled *time.Time `json:"uninstalled" yaml:"uninstalled" toml:"uninstalled"`
 
 	// BundleReference of the bundle that last altered the installation state.
 	BundleReference string `json:"bundleReference" yaml:"bundleReference" toml:"bundleReference"`
diff --git a/pkg/storage/installation_provider.go b/pkg/storage/installation_provider.go
index 6c8111c1..9d0b6b92 100644
--- a/pkg/storage/installation_provider.go
+++ b/pkg/storage/installation_provider.go
@@ -35,7 +35,7 @@ type InstallationProvider interface {
 	GetInstallation(ctx context.Context, namespace string, name string) (Installation, error)
 
 	// ListInstallations returns Installations sorted in ascending order by the namespace and then name.
-	ListInstallations(ctx context.Context, namespace string, name string, labels map[string]string) ([]Installation, error)
+	ListInstallations(ctx context.Context, listOption ListOptions) ([]Installation, error)
 
 	// ListRuns returns Run documents sorted in ascending order by ID.
 	ListRuns(ctx context.Context, namespace string, installation string) ([]Run, map[string][]Result, error)
diff --git a/pkg/storage/installation_store.go b/pkg/storage/installation_store.go
index 9dc4cf66..04cc62b6 100644
--- a/pkg/storage/installation_store.go
+++ b/pkg/storage/installation_store.go
@@ -63,17 +63,12 @@ func (s InstallationStore) Initialize(ctx context.Context) error {
 	return span.Error(err)
 }
 
-func (s InstallationStore) ListInstallations(ctx context.Context, namespace string, name string, labels map[string]string) ([]Installation, error) {
+func (s InstallationStore) ListInstallations(ctx context.Context, listOptions ListOptions) ([]Installation, error) {
 	_, log := tracing.StartSpan(ctx)
 	defer log.EndSpan()
 
 	var out []Installation
-	findOpts := FindOptions{
-		Sort:   []string{"namespace", "name"},
-		Filter: CreateListFiler(namespace, name, labels),
-	}
-
-	err := s.store.Find(ctx, CollectionInstallations, findOpts, &out)
+	err := s.store.Find(ctx, CollectionInstallations, listOptions.ToFindOptions(), &out)
 	return out, err
 }
 
@@ -217,21 +212,21 @@ func (s InstallationStore) GetLastOutputs(ctx context.Context, namespace string,
 	opts := AggregateOptions{
 		Pipeline: []bson.D{
 			// List outputs by installation
-			{{"$match", bson.M{
+			{{Key: "$match", Value: bson.M{
 				"namespace":    namespace,
 				"installation": installation,
 			}}},
 			// Reverse sort them (newest on top)
-			{{"$sort", bson.D{
-				{"namespace", 1},
-				{"installation", 1},
-				{"name", 1},
-				{"resultId", -1},
+			{{Key: "$sort", Value: bson.D{
+				{Key: "namespace", Value: 1},
+				{Key: "installation", Value: 1},
+				{Key: "name", Value: 1},
+				{Key: "resultId", Value: -1},
 			}}},
 			// Group them by output name and select the last value for each output
-			{{"$group", bson.D{
-				{"_id", "$name"},
-				{"lastOutput", bson.M{"$first": "$$ROOT"}},
+			{{Key: "$group", Value: bson.D{
+				{Key: "_id", Value: "$name"},
+				{Key: "lastOutput", Value: bson.M{"$first": "$$ROOT"}},
 			}}},
 		},
 	}
diff --git a/pkg/storage/parameter_store.go b/pkg/storage/parameter_store.go
index e1c641a2..658cf9e9 100644
--- a/pkg/storage/parameter_store.go
+++ b/pkg/storage/parameter_store.go
@@ -100,12 +100,9 @@ func (s ParameterStore) InsertParameterSet(ctx context.Context, params Parameter
 	return s.Documents.Insert(ctx, CollectionParameters, opts)
 }
 
-func (s ParameterStore) ListParameterSets(ctx context.Context, namespace string, name string, labels map[string]string) ([]ParameterSet, error) {
+func (s ParameterStore) ListParameterSets(ctx context.Context, listOptions ListOptions) ([]ParameterSet, error) {
 	var out []ParameterSet
-	opts := FindOptions{
-		Filter: CreateListFiler(namespace, name, labels),
-	}
-	err := s.Documents.Find(ctx, CollectionParameters, opts, &out)
+	err := s.Documents.Find(ctx, CollectionParameters, listOptions.ToFindOptions(), &out)
 	return out, err
 }
 
diff --git a/pkg/storage/parameterset_provider.go b/pkg/storage/parameterset_provider.go
index 86944f8a..64b35c3d 100644
--- a/pkg/storage/parameterset_provider.go
+++ b/pkg/storage/parameterset_provider.go
@@ -17,7 +17,7 @@ type ParameterSetProvider interface {
 	Validate(ctx context.Context, params ParameterSet) error
 
 	InsertParameterSet(ctx context.Context, params ParameterSet) error
-	ListParameterSets(ctx context.Context, namespace string, name string, labels map[string]string) ([]ParameterSet, error)
+	ListParameterSets(ctx context.Context, listOptions ListOptions) ([]ParameterSet, error)
 	GetParameterSet(ctx context.Context, namespace string, name string) (ParameterSet, error)
 	UpdateParameterSet(ctx context.Context, params ParameterSet) error
 	UpsertParameterSet(ctx context.Context, params ParameterSet) error
diff --git a/pkg/storage/query.go b/pkg/storage/query.go
index 0e686d2a..c6e9f33c 100644
--- a/pkg/storage/query.go
+++ b/pkg/storage/query.go
@@ -57,7 +57,7 @@ func convertSortKeys(values []string) bson.D {
 			sortKey = strings.Trim(key, "-")
 			sortOrder = -1
 		}
-		keys[i] = bson.E{sortKey, sortOrder}
+		keys[i] = bson.E{Key: sortKey, Value: sortOrder}
 	}
 	return keys
 }
@@ -288,20 +288,45 @@ func convertToRawJsonDocument(in interface{}, raw interface{}) error {
 	return json.Unmarshal(data, raw)
 }
 
-// CreateListFiler builds a query for a list of documents by:
-// * matching namespace
-// * name contains substring
-// * labels contains all matches
-func CreateListFiler(namespace string, name string, labels map[string]string) map[string]interface{} {
+// ListOptions is the set of options available to the list operation
+// on any storage provider.
+type ListOptions struct {
+	// Namespace in which the particular result list is defined.
+	Namespace string
+
+	// Name specifies whether the result list name contain the specified substring.
+	Name string
+
+	// Labels is used to filter result list based on a key-value pair.
+	Labels map[string]string
+
+	// Skip is the number of results to skip past and exclude from the results.
+	Skip int64
+
+	// Limit is the number of results to return.
+	Limit int64
+}
+
+// ToFindOptions builds a query for a list of documents with these conditions:
+// * sorted in ascending order by namespace first and then name
+// * filtered by matching namespace, name contains substring, and labels contain all matches
+// * skipped and limited to a certain number of result
+func (o ListOptions) ToFindOptions() FindOptions {
 	filter := make(map[string]interface{}, 3)
-	if namespace != "*" {
-		filter["namespace"] = namespace
+	if o.Namespace != "*" {
+		filter["namespace"] = o.Namespace
 	}
-	if name != "" {
-		filter["name"] = map[string]interface{}{"$regex": name}
+	if o.Name != "" {
+		filter["name"] = map[string]interface{}{"$regex": o.Name}
 	}
-	for k, v := range labels {
+	for k, v := range o.Labels {
 		filter["labels."+k] = v
 	}
-	return filter
+
+	return FindOptions{
+		Sort:   []string{"namespace", "name"},
+		Filter: filter,
+		Skip:   o.Skip,
+		Limit:  o.Limit,
+	}
 }
diff --git a/pkg/storage/run.go b/pkg/storage/run.go
index 299e8139..ca1751b1 100644
--- a/pkg/storage/run.go
+++ b/pkg/storage/run.go
@@ -16,36 +16,36 @@ type Run struct {
 	SchemaVersion schema.Version `json:"schemaVersion" yaml:"schemaVersion" toml:"schemaVersion"`
 
 	// ID of the Run.
-	ID string `json:"_id" yaml:"_id", toml:"_id"`
+	ID string `json:"_id" yaml:"_id" toml:"_id"`
 
 	// Created timestamp of the Run.
-	Created time.Time `json:"created" yaml:"created", toml:"created"`
+	Created time.Time `json:"created" yaml:"created" toml:"created"`
 
 	// Namespace of the installation.
-	Namespace string `json:"namespace" yaml:"namespace", toml:"namespace"`
+	Namespace string `json:"namespace" yaml:"namespace" toml:"namespace"`
 
 	// Installation name.
-	Installation string `json:"installation" yaml:"installation", toml:"installation"`
+	Installation string `json:"installation" yaml:"installation" toml:"installation"`
 
 	// Revision of the installation.
-	Revision string `json:"revision" yaml:"revision", toml:"revision"`
+	Revision string `json:"revision" yaml:"revision" toml:"revision"`
 
 	// Action executed against the installation.
-	Action string `json:"action" yaml:"action", toml:"action"`
+	Action string `json:"action" yaml:"action" toml:"action"`
 
 	// Bundle is the definition of the bundle.
-	Bundle bundle.Bundle `json:"bundle" yaml:"bundle", toml:"bundle"`
+	Bundle bundle.Bundle `json:"bundle" yaml:"bundle" toml:"bundle"`
 
 	// BundleReference is the canonical reference to the bundle used in the action.
-	BundleReference string `json:"bundleReference" yaml:"bundleReference", toml:"bundleReference"`
+	BundleReference string `json:"bundleReference" yaml:"bundleReference" toml:"bundleReference"`
 
 	// BundleDigest is the digest of the bundle.
 	// TODO(carolynvs): populate this
-	BundleDigest string `json:"bundleDigest" yaml:"bundleDigest", toml:"bundleDigest"`
+	BundleDigest string `json:"bundleDigest" yaml:"bundleDigest" toml:"bundleDigest"`
 
 	// ParameterOverrides are the key/value parameter overrides (taking precedence over
 	// parameters specified in a parameter set) specified during the run.
-	ParameterOverrides ParameterSet `json:"parameterOverrides, omitempty" yaml:"parameterOverrides, omitempty", toml:"parameterOverrides, omitempty"`
+	ParameterOverrides ParameterSet `json:"parameterOverrides,omitempty" yaml:"parameterOverrides,omitempty" toml:"parameterOverrides,omitempty"`
 
 	// CredentialSets is a list of the credential set names used during the run.
 	CredentialSets []string `json:"credentialSets,omitempty" yaml:"credentialSets,omitempty" toml:"credentialSets,omitempty"`
@@ -61,7 +61,7 @@ type Run struct {
 
 	// Custom extension data applicable to a given runtime.
 	// TODO(carolynvs): remove custom and populate it in ToCNAB
-	Custom interface{} `json:"custom" yaml:"custom", toml:"custom"`
+	Custom interface{} `json:"custom" yaml:"custom" toml:"custom"`
 }
 
 func (r Run) DefaultDocumentFilter() map[string]interface{} {
@@ -118,7 +118,7 @@ func (r Run) ToCNAB() cnab.Claim {
 // TypedParameterValues returns parameters values that have been converted to
 // its typed value based on its bundle definition.
 func (r Run) TypedParameterValues() map[string]interface{} {
-	bun := cnab.ExtendedBundle{r.Bundle}
+	bun := cnab.NewBundle(r.Bundle)
 	value := make(map[string]interface{})
 
 	for _, param := range r.Parameters.Parameters {


Test patch:
diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go
index 31ac57b8..77c14277 100644
--- a/pkg/cache/cache_test.go
+++ b/pkg/cache/cache_test.go
@@ -196,25 +196,25 @@ func TestStoreManifest(t *testing.T) {
 	}{
 		{
 			name: "embedded manifest",
-			bundle: cnab.ExtendedBundle{bundle.Bundle{
+			bundle: cnab.NewBundle(bundle.Bundle{
 				Custom: map[string]interface{}{
 					"sh.porter": map[string]interface{}{
 						"manifest": "bmFtZTogSEVMTE9fQ1VTVE9NCnZlcnNpb246IDAuMS4wCmRlc2NyaXB0aW9uOiAiQSBidW5kbGUgd2l0aCBhIGN1c3RvbSBhY3Rpb24iCnRhZzogZ2V0cG9ydGVyL3BvcnRlci1oZWxsbzp2MC4xLjAKaW52b2NhdGlvbkltYWdlOiBnZXRwb3J0ZXIvcG9ydGVyLWhlbGxvLWluc3RhbGxlcjowLjEuMAoKY3JlZGVudGlhbHM6CiAgLSBuYW1lOiBteS1maXJzdC1jcmVkCiAgICBlbnY6IE1ZX0ZJUlNUX0NSRUQKICAtIG5hbWU6IG15LXNlY29uZC1jcmVkCiAgICBkZXNjcmlwdGlvbjogIk15IHNlY29uZCBjcmVkIgogICAgcGF0aDogL3BhdGgvdG8vbXktc2Vjb25kLWNyZWQKCmltYWdlczogCiAgIHNvbWV0aGluZzoKICAgICAgZGVzY3JpcHRpb246ICJhbiBpbWFnZSIKICAgICAgaW1hZ2VUeXBlOiAiZG9ja2VyIgogICAgICByZXBvc2l0b3J5OiAiZ2V0cG9ydGVyL2JvbyIKCnBhcmFtZXRlcnM6CiAgLSBuYW1lOiBteS1maXJzdC1wYXJhbQogICAgdHlwZTogaW50ZWdlcgogICAgZGVmYXVsdDogOQogICAgZW52OiBNWV9GSVJTVF9QQVJBTQogICAgYXBwbHlUbzoKICAgICAgLSAiaW5zdGFsbCIKICAtIG5hbWU6IG15LXNlY29uZC1wYXJhbQogICAgZGVzY3JpcHRpb246ICJNeSBzZWNvbmQgcGFyYW1ldGVyIgogICAgdHlwZTogc3RyaW5nCiAgICBkZWZhdWx0OiBzcHJpbmctbXVzaWMtZGVtbwogICAgcGF0aDogL3BhdGgvdG8vbXktc2Vjb25kLXBhcmFtCiAgICBzZW5zaXRpdmU6IHRydWUKCm91dHB1dHM6CiAgLSBuYW1lOiBteS1maXJzdC1vdXRwdXQKICAgIHR5cGU6IHN0cmluZwogICAgYXBwbHlUbzoKICAgICAgLSAiaW5zdGFsbCIKICAgICAgLSAidXBncmFkZSIKICAgIHNlbnNpdGl2ZTogdHJ1ZQogIC0gbmFtZTogbXktc2Vjb25kLW91dHB1dAogICAgZGVzY3JpcHRpb246ICJNeSBzZWNvbmQgb3V0cHV0IgogICAgdHlwZTogYm9vbGVhbgogICAgc2Vuc2l0aXZlOiBmYWxzZQogIC0gbmFtZToga3ViZWNvbmZpZwogICAgdHlwZTogZmlsZQogICAgcGF0aDogL3Jvb3QvLmt1YmUvY29uZmlnCgptaXhpbnM6CiAgLSBleGVjCgppbnN0YWxsOgogIC0gZXhlYzoKICAgICAgZGVzY3JpcHRpb246ICJJbnN0YWxsIEhlbGxvIFdvcmxkIgogICAgICBjb21tYW5kOiBiYXNoCiAgICAgIGZsYWdzOgogICAgICAgIGM6IGVjaG8gSGVsbG8gV29ybGQKCnVwZ3JhZGU6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIldvcmxkIDIuMCIKICAgICAgY29tbWFuZDogYmFzaAogICAgICBmbGFnczoKICAgICAgICBjOiBlY2hvIFdvcmxkIDIuMAoKem9tYmllczoKICAtIGV4ZWM6CiAgICAgIGRlc2NyaXB0aW9uOiAiVHJpZ2dlciB6b21iaWUgYXBvY2FseXBzZSIKICAgICAgY29tbWFuZDogYmFzaAogICAgICBmbGFnczoKICAgICAgICBjOiBlY2hvIG9oIG5vZXMgbXkgYnJhaW5zCgp1bmluc3RhbGw6CiAgLSBleGVjOgogICAgICBkZXNjcmlwdGlvbjogIlVuaW5zdGFsbCBIZWxsbyBXb3JsZCIKICAgICAgY29tbWFuZDogYmFzaAogICAgICBmbGFnczoKICAgICAgICBjOiBlY2hvIEdvb2RieWUgV29ybGQK",
 					},
 				},
-			}},
+			}),
 			tag:                 kahn1dot01,
 			shouldCacheManifest: true,
 		},
 		{
 			name: "porter stamp, no manifest",
-			bundle: cnab.ExtendedBundle{bundle.Bundle{
+			bundle: cnab.NewBundle(bundle.Bundle{
 				Custom: map[string]interface{}{
 					"sh.porter": map[string]interface{}{
 						"manifestDigest": "abc123",
 					},
 				},
-			}},
+			}),
 			tag:                 kahn1dot01,
 			shouldCacheManifest: false,
 		},
diff --git a/pkg/cnab/config-adapter/adapter_test.go b/pkg/cnab/config-adapter/adapter_test.go
index 8f75a5bd..13ee026a 100644
--- a/pkg/cnab/config-adapter/adapter_test.go
+++ b/pkg/cnab/config-adapter/adapter_test.go
@@ -659,7 +659,7 @@ func TestNewManifestConverter_generateOutputWiringParameter(t *testing.T) {
 	outputDef := definition.Schema{
 		Type: "string",
 	}
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Outputs: map[string]bundle.Output{
 			"msg": {
 				Definition: "stringDef",
@@ -671,7 +671,7 @@ func TestNewManifestConverter_generateOutputWiringParameter(t *testing.T) {
 		Definitions: map[string]*definition.Schema{
 			"stringDef": &outputDef,
 		},
-	}}
+	})
 
 	t.Run("generate parameter", func(t *testing.T) {
 		t.Parallel()
diff --git a/pkg/cnab/config-adapter/stamp_test.go b/pkg/cnab/config-adapter/stamp_test.go
index d39fb1a0..a9a3526c 100644
--- a/pkg/cnab/config-adapter/stamp_test.go
+++ b/pkg/cnab/config-adapter/stamp_test.go
@@ -50,7 +50,7 @@ func TestConfig_GenerateStamp(t *testing.T) {
 func TestConfig_LoadStamp(t *testing.T) {
 	t.Parallel()
 
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			config.CustomPorterKey: map[string]interface{}{
 				"manifestDigest": "somedigest",
@@ -60,7 +60,7 @@ func TestConfig_LoadStamp(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	stamp, err := LoadStamp(bun)
 	require.NoError(t, err)
@@ -72,13 +72,13 @@ func TestConfig_LoadStamp(t *testing.T) {
 func TestConfig_LoadStamp_Invalid(t *testing.T) {
 	t.Parallel()
 
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			config.CustomPorterKey: []string{
 				"somedigest",
 			},
 		},
-	}}
+	})
 
 	stamp, err := LoadStamp(bun)
 	require.Error(t, err)
diff --git a/pkg/cnab/dependencies_test.go b/pkg/cnab/dependencies_test.go
index 32711630..0c12694a 100644
--- a/pkg/cnab/dependencies_test.go
+++ b/pkg/cnab/dependencies_test.go
@@ -18,7 +18,7 @@ func TestReadDependencyProperties(t *testing.T) {
 	b, err := bundle.Unmarshal(data)
 	require.NoError(t, err, "could not unmarshal the bundle")
 
-	bun := ExtendedBundle{*b}
+	bun := NewBundle(*b)
 	assert.True(t, bun.HasDependencies())
 
 	deps, err := bun.ReadDependencies()
@@ -44,7 +44,7 @@ func TestDependencies_ListBySequence(t *testing.T) {
 
 	sequenceMock := []string{"nginx", "storage", "mysql"}
 
-	bun := ExtendedBundle{bundle.Bundle{
+	bun := NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			DependenciesExtensionKey: Dependencies{
 				Sequence: sequenceMock,
@@ -68,7 +68,7 @@ func TestDependencies_ListBySequence(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	rawDeps, err := bun.ReadDependencies()
 	orderedDeps := rawDeps.ListBySequence()
@@ -92,9 +92,9 @@ func TestSupportsDependencies(t *testing.T) {
 	t.Parallel()
 
 	t.Run("supported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{DependenciesExtensionKey},
-		}}
+		})
 
 		assert.True(t, b.SupportsDependencies())
 	})
@@ -109,19 +109,19 @@ func TestHasDependencies(t *testing.T) {
 	t.Parallel()
 
 	t.Run("has dependencies", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{DependenciesExtensionKey},
 			Custom: map[string]interface{}{
 				DependenciesExtensionKey: struct{}{},
 			},
-		}}
+		})
 
 		assert.True(t, b.HasDependencies())
 	})
 	t.Run("no dependencies", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{DependenciesExtensionKey},
-		}}
+		})
 
 		assert.False(t, b.HasDependencies())
 	})
diff --git a/pkg/cnab/docker_test.go b/pkg/cnab/docker_test.go
index 83f56325..59338725 100644
--- a/pkg/cnab/docker_test.go
+++ b/pkg/cnab/docker_test.go
@@ -55,14 +55,14 @@ func TestSupportsDocker(t *testing.T) {
 	t.Parallel()
 
 	t.Run("supported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{DockerExtensionKey},
-		}}
+		})
 
 		assert.True(t, b.SupportsDocker())
 	})
 	t.Run("unsupported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{}}
+		b := NewBundle(bundle.Bundle{})
 
 		assert.False(t, b.SupportsDocker())
 	})
diff --git a/pkg/cnab/extended_bundle_test.go b/pkg/cnab/extended_bundle_test.go
index 37cb04a2..b53c22e6 100644
--- a/pkg/cnab/extended_bundle_test.go
+++ b/pkg/cnab/extended_bundle_test.go
@@ -11,11 +11,11 @@ import (
 
 func TestExtendedBundle_IsPorterBundle(t *testing.T) {
 	t.Run("made by porter", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			Custom: map[string]interface{}{
 				"sh.porter": struct{}{},
 			},
-		}}
+		})
 
 		assert.True(t, b.IsPorterBundle())
 	})
@@ -35,7 +35,7 @@ func TestExtendedBundle_IsFileType(t *testing.T) {
 		Type:            "string",
 		ContentEncoding: "base64",
 	}
-	bun := ExtendedBundle{bundle.Bundle{
+	bun := NewBundle(bundle.Bundle{
 		RequiredExtensions: []string{
 			FileParameterExtensionKey,
 		},
@@ -52,7 +52,7 @@ func TestExtendedBundle_IsFileType(t *testing.T) {
 				Definition: "file",
 			},
 		},
-	}}
+	})
 
 	assert.False(t, bun.IsFileType(stringDef), "strings should not be flagged as files")
 	assert.True(t, bun.IsFileType(fileDef), "strings+base64 with the file-parameters extension should be categorized as files")
@@ -70,7 +70,7 @@ func TestExtendedBundle_IsFileType(t *testing.T) {
 }
 
 func TestExtendedBundle_IsInternalParameter(t *testing.T) {
-	bun := ExtendedBundle{bundle.Bundle{
+	bun := NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"foo": &definition.Schema{
 				Type: "string",
@@ -91,7 +91,7 @@ func TestExtendedBundle_IsInternalParameter(t *testing.T) {
 				Definition: "porter-debug",
 			},
 		},
-	}}
+	})
 
 	t.Run("empty bundle", func(t *testing.T) {
 		b := ExtendedBundle{}
@@ -117,7 +117,7 @@ func TestExtendedBundle_IsInternalParameter(t *testing.T) {
 
 func TestExtendedBundle_IsSensitiveParameter(t *testing.T) {
 	sensitive := true
-	bun := ExtendedBundle{bundle.Bundle{
+	bun := NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"foo": &definition.Schema{
 				Type:      "string",
@@ -139,7 +139,7 @@ func TestExtendedBundle_IsSensitiveParameter(t *testing.T) {
 				Definition: "porter-debug",
 			},
 		},
-	}}
+	})
 
 	t.Run("empty bundle", func(t *testing.T) {
 		b := ExtendedBundle{}
diff --git a/pkg/cnab/extensions_test.go b/pkg/cnab/extensions_test.go
index 8fc99b13..4045244d 100644
--- a/pkg/cnab/extensions_test.go
+++ b/pkg/cnab/extensions_test.go
@@ -9,13 +9,13 @@ import (
 
 func TestSupportsExtension(t *testing.T) {
 	t.Run("key present", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{RequiredExtensions: []string{"io.test.thing"}}}
+		b := NewBundle(bundle.Bundle{RequiredExtensions: []string{"io.test.thing"}})
 		assert.True(t, b.SupportsExtension("io.test.thing"))
 	})
 
 	t.Run("key missing", func(t *testing.T) {
 		// We need to match against the full key, not just shorthand
-		b := ExtendedBundle{bundle.Bundle{RequiredExtensions: []string{"thing"}}}
+		b := NewBundle(bundle.Bundle{RequiredExtensions: []string{"thing"}})
 		assert.False(t, b.SupportsExtension("io.test.thing"))
 	})
 }
diff --git a/pkg/cnab/file_parameter_test.go b/pkg/cnab/file_parameter_test.go
index 8b8565d9..3ac83127 100644
--- a/pkg/cnab/file_parameter_test.go
+++ b/pkg/cnab/file_parameter_test.go
@@ -35,14 +35,14 @@ func TestSupportsFileParameters(t *testing.T) {
 	t.Parallel()
 
 	t.Run("supported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{FileParameterExtensionKey},
-		}}
+		})
 
 		assert.True(t, b.SupportsFileParameters())
 	})
 	t.Run("unsupported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{}}
+		b := NewBundle(bundle.Bundle{})
 
 		assert.False(t, b.SupportsFileParameters())
 	})
diff --git a/pkg/cnab/parameter_sources_test.go b/pkg/cnab/parameter_sources_test.go
index 6ee613fe..228878c5 100644
--- a/pkg/cnab/parameter_sources_test.go
+++ b/pkg/cnab/parameter_sources_test.go
@@ -60,7 +60,7 @@ func TestReadParameterSourcesProperties(t *testing.T) {
 
 	b, err := bundle.Unmarshal(data)
 	require.NoError(t, err, "could not unmarshal the bundle")
-	bun := ExtendedBundle{*b}
+	bun := NewBundle(*b)
 	assert.True(t, bun.HasParameterSources())
 
 	ps, err := bun.ReadParameterSources()
@@ -88,9 +88,9 @@ func TestSupportsParameterSources(t *testing.T) {
 	t.Parallel()
 
 	t.Run("supported", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{ParameterSourcesExtensionKey},
-		}}
+		})
 
 		assert.True(t, b.SupportsParameterSources())
 	})
@@ -105,19 +105,19 @@ func TestHasParameterSources(t *testing.T) {
 	t.Parallel()
 
 	t.Run("has parameter sources", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{ParameterSourcesExtensionKey},
 			Custom: map[string]interface{}{
 				ParameterSourcesExtensionKey: struct{}{},
 			},
-		}}
+		})
 
 		assert.True(t, b.HasParameterSources())
 	})
 	t.Run("no parameter sources", func(t *testing.T) {
-		b := ExtendedBundle{bundle.Bundle{
+		b := NewBundle(bundle.Bundle{
 			RequiredExtensions: []string{ParameterSourcesExtensionKey},
-		}}
+		})
 
 		assert.False(t, b.HasParameterSources())
 	})
diff --git a/pkg/cnab/provider/credentials_test.go b/pkg/cnab/provider/credentials_test.go
index 4f7004bc..8a03effa 100644
--- a/pkg/cnab/provider/credentials_test.go
+++ b/pkg/cnab/provider/credentials_test.go
@@ -34,7 +34,7 @@ func TestRuntime_loadCredentials(t *testing.T) {
 	err := r.credentials.InsertCredentialSet(context.Background(), cs1)
 	require.NoError(t, err, "Save credential set failed")
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Credentials: map[string]bundle.Credential{
 			"password": {
 				Location: bundle.Location{
@@ -47,7 +47,7 @@ func TestRuntime_loadCredentials(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	args := ActionArguments{Installation: storage.Installation{CredentialSets: []string{"mycreds"}}, Action: "install"}
 	gotValues, err := r.loadCredentials(context.Background(), b, args)
@@ -65,7 +65,7 @@ func TestRuntime_loadCredentials(t *testing.T) {
 
 func TestRuntime_loadCredentials_WithApplyTo(t *testing.T) {
 	getBundle := func(required bool) cnab.ExtendedBundle {
-		return cnab.ExtendedBundle{bundle.Bundle{
+		return cnab.ExtendedBundle{Bundle: bundle.Bundle{
 			Credentials: map[string]bundle.Credential{
 				"password": {
 					Location: bundle.Location{
diff --git a/pkg/cnab/solver_test.go b/pkg/cnab/solver_test.go
index 7bb4018f..c211f565 100644
--- a/pkg/cnab/solver_test.go
+++ b/pkg/cnab/solver_test.go
@@ -11,7 +11,7 @@ import (
 func TestDependencySolver_ResolveDependencies(t *testing.T) {
 	t.Parallel()
 
-	bun := ExtendedBundle{bundle.Bundle{
+	bun := NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			DependenciesExtensionKey: Dependencies{
 				Requires: map[string]Dependency{
@@ -24,7 +24,7 @@ func TestDependencySolver_ResolveDependencies(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	s := DependencySolver{}
 	locks, err := s.ResolveDependencies(bun)
diff --git a/pkg/generator/parameters_test.go b/pkg/generator/parameters_test.go
index 2536f356..9743f8e5 100644
--- a/pkg/generator/parameters_test.go
+++ b/pkg/generator/parameters_test.go
@@ -33,7 +33,7 @@ func TestGoodParametersName(t *testing.T) {
 			Name:   name,
 			Silent: true,
 		},
-		Bundle: cnab.ExtendedBundle{bundle.Bundle{
+		Bundle: cnab.NewBundle(bundle.Bundle{
 			Parameters: map[string]bundle.Parameter{
 				"one": {
 					Definition: "one",
@@ -46,7 +46,7 @@ func TestGoodParametersName(t *testing.T) {
 				},
 			},
 		},
-		}}
+		)}
 
 	pset, err := opts.GenerateParameters()
 	require.NoError(t, err, "name should NOT have resulted in an error")
@@ -98,7 +98,7 @@ func TestSkipParameters(t *testing.T) {
 			Name:   name,
 			Silent: true,
 		},
-		Bundle: cnab.ExtendedBundle{bundle.Bundle{
+		Bundle: cnab.NewBundle(bundle.Bundle{
 			Definitions: definition.Definitions{
 				"porter-debug": &definition.Schema{
 					Comment: cnab.PorterInternal,
@@ -110,7 +110,7 @@ func TestSkipParameters(t *testing.T) {
 				},
 			},
 		},
-		}}
+		)}
 
 	pset, err := opts.GenerateParameters()
 	require.NoError(t, err, "parameters generation should not have resulted in an error")
diff --git a/pkg/plugins/pluggable/loader_test.go b/pkg/plugins/pluggable/loader_test.go
index 882678a1..10b18896 100644
--- a/pkg/plugins/pluggable/loader_test.go
+++ b/pkg/plugins/pluggable/loader_test.go
@@ -51,7 +51,7 @@ func TestPluginLoader_SelectPlugin(t *testing.T) {
 		c.Data.DefaultStorage = "azure"
 		c.Data.StoragePlugins = []config.StoragePlugin{
 			{
-				config.PluginConfig{
+				PluginConfig: config.PluginConfig{
 					Name:         "azure",
 					PluginSubKey: "azure.blob",
 					Config: map[string]interface{}{
diff --git a/pkg/porter/archive_test.go b/pkg/porter/archive_test.go
index 12905cba..3a294c22 100644
--- a/pkg/porter/archive_test.go
+++ b/pkg/porter/archive_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	"get.porter.sh/porter/pkg"
+	"get.porter.sh/porter/tests"
 	"github.com/stretchr/testify/require"
 )
 
@@ -51,3 +53,17 @@ func TestArchive_Validate(t *testing.T) {
 		})
 	}
 }
+
+func TestArchive_ArchiveDirectory(t *testing.T) {
+	p := NewTestPorter(t)
+	defer p.Close()
+	ex := exporter{
+		fs: p.FileSystem,
+	}
+
+	dir, err := ex.createArchiveFolder("examples/test-bundle-0.2.0")
+	require.NoError(t, err)
+	require.Contains(t, dir, "examples-test-bundle-0.2.0")
+
+	tests.AssertDirectoryPermissionsEqual(t, dir, pkg.FileModeDirectory)
+}
diff --git a/pkg/porter/examples/capture_output_example_test.go b/pkg/porter/examples/capture_output_example_test.go
index ca36d77b..317ec2dd 100644
--- a/pkg/porter/examples/capture_output_example_test.go
+++ b/pkg/porter/examples/capture_output_example_test.go
@@ -9,7 +9,7 @@ import (
 	"get.porter.sh/porter/pkg/porter"
 )
 
-func ExampleCaptureOutput() {
+func ExamplePorter_captureOutput() {
 	// Create an instance of the Porter application
 	p := porter.New()
 
diff --git a/pkg/porter/examples/install_example_test.go b/pkg/porter/examples/install_example_test.go
index bf33fb2c..5675d0bb 100644
--- a/pkg/porter/examples/install_example_test.go
+++ b/pkg/porter/examples/install_example_test.go
@@ -8,7 +8,7 @@ import (
 	"get.porter.sh/porter/pkg/porter"
 )
 
-func ExampleInstall() {
+func ExamplePorter_install() {
 	// Create an instance of the Porter application
 	p := porter.New()
 
diff --git a/pkg/porter/examples/pull_example_test.go b/pkg/porter/examples/pull_example_test.go
index 1241dd0d..bcc858a9 100644
--- a/pkg/porter/examples/pull_example_test.go
+++ b/pkg/porter/examples/pull_example_test.go
@@ -7,7 +7,7 @@ import (
 	"get.porter.sh/porter/pkg/porter"
 )
 
-func ExamplePullBundle() {
+func ExamplePorter_pullBundle() {
 	// Create an instance of the Porter application
 	p := porter.New()
 
diff --git a/pkg/porter/explain_test.go b/pkg/porter/explain_test.go
index 152779db..6a32e3d1 100644
--- a/pkg/porter/explain_test.go
+++ b/pkg/porter/explain_test.go
@@ -110,7 +110,7 @@ func TestExplain_generateYAML(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundleParams(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		RequiredExtensions: []string{
 			cnab.FileParameterExtensionKey,
 		},
@@ -140,7 +140,7 @@ func TestExplain_generatePrintableBundleParams(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	pb, err := generatePrintable(bun, "")
 	require.NoError(t, err)
@@ -160,7 +160,7 @@ func TestExplain_generatePrintableBundleParams(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundleParamsWithAction(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		RequiredExtensions: []string{
 			cnab.FileParameterExtensionKey,
 		},
@@ -191,7 +191,7 @@ func TestExplain_generatePrintableBundleParamsWithAction(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	t.Run("action applies", func(t *testing.T) {
 		pb, err := generatePrintable(bun, "install")
@@ -233,7 +233,7 @@ func TestExplain_generatePrintableBundleParamsWithAction(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundleOutputs(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"string": &definition.Schema{
 				Type:    "string",
@@ -256,7 +256,7 @@ func TestExplain_generatePrintableBundleOutputs(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	pb, err := generatePrintable(bun, "")
 	require.NoError(t, err)
@@ -288,7 +288,7 @@ func TestExplain_generatePrintableBundleOutputs(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundleCreds(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Credentials: map[string]bundle.Credential{
 			"kubeconfig": {
 				Required:    true,
@@ -306,7 +306,7 @@ func TestExplain_generatePrintableBundleCreds(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	t.Run("action applies", func(t *testing.T) {
 		pb, err := generatePrintable(bun, "install")
@@ -348,7 +348,7 @@ func TestExplain_generatePrintableBundleCreds(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundlePorterVersion(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"string": &definition.Schema{
 				Type:    "string",
@@ -362,7 +362,7 @@ func TestExplain_generatePrintableBundlePorterVersion(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	pb, err := generatePrintable(bun, "")
 	assert.NoError(t, err)
@@ -371,14 +371,14 @@ func TestExplain_generatePrintableBundlePorterVersion(t *testing.T) {
 }
 
 func TestExplain_generatePrintableBundlePorterVersionNonPorterBundle(t *testing.T) {
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"string": &definition.Schema{
 				Type:    "string",
 				Default: "clippy",
 			},
 		},
-	}}
+	})
 
 	pb, err := generatePrintable(bun, "")
 	assert.NoError(t, err)
@@ -389,7 +389,7 @@ func TestExplain_generatePrintableBundlePorterVersionNonPorterBundle(t *testing.
 func TestExplain_generatePrintableBundleDependencies(t *testing.T) {
 
 	sequenceMock := []string{"nginx", "storage", "mysql"}
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			cnab.DependenciesExtensionKey: cnab.Dependencies{
 				Sequence: sequenceMock,
@@ -414,7 +414,7 @@ func TestExplain_generatePrintableBundleDependencies(t *testing.T) {
 				"commit":   "3b7c85ba",
 			},
 		},
-	}}
+	})
 
 	pd, err := generatePrintable(bun, "")
 	assert.NoError(t, err)
diff --git a/pkg/porter/outputs_test.go b/pkg/porter/outputs_test.go
index a4dafa13..43999c62 100644
--- a/pkg/porter/outputs_test.go
+++ b/pkg/porter/outputs_test.go
@@ -103,7 +103,7 @@ func TestPorter_PrintBundleOutputs(t *testing.T) {
 				},
 			}
 
-			extB := cnab.ExtendedBundle{b}
+			extB := cnab.NewBundle(b)
 			i := p.TestInstallations.CreateInstallation(storage.NewInstallation("", "test"), func(i *storage.Installation) {
 				i.Parameters.Parameters = p.SanitizeParameters(i.Parameters.Parameters, i.ID, extB)
 			})
diff --git a/pkg/porter/parameters_test.go b/pkg/porter/parameters_test.go
index 9186e4dd..9d02726d 100644
--- a/pkg/porter/parameters_test.go
+++ b/pkg/porter/parameters_test.go
@@ -99,9 +99,9 @@ func Test_loadParameters_paramNotDefined(t *testing.T) {
 	r := NewTestPorter(t)
 	defer r.Close()
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Parameters: map[string]bundle.Parameter{},
-	}}
+	})
 
 	overrides := map[string]string{
 		"foo": "bar",
@@ -118,13 +118,13 @@ func Test_loadParameters_definitionNotDefined(t *testing.T) {
 	r := NewTestPorter(t)
 	defer r.Close()
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Parameters: map[string]bundle.Parameter{
 			"foo": {
 				Definition: "foo",
 			},
 		},
-	}}
+	})
 
 	overrides := map[string]string{
 		"foo": "bar",
@@ -143,7 +143,7 @@ func Test_loadParameters_applyTo(t *testing.T) {
 
 	// Here we set default values, but expect nil/empty
 	// values for parameters that do not apply to a given action
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"foo": &definition.Schema{
 				Type:    "string",
@@ -175,7 +175,7 @@ func Test_loadParameters_applyTo(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	overrides := map[string]string{
 		"foo":  "FOO",
@@ -198,7 +198,7 @@ func Test_loadParameters_applyToBundleDefaults(t *testing.T) {
 	r := NewTestPorter(t)
 	defer r.Close()
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"foo": &definition.Schema{
 				Type:    "string",
@@ -213,7 +213,7 @@ func Test_loadParameters_applyToBundleDefaults(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	i := storage.Installation{}
 	params, err := r.resolveParameters(context.Background(), i, b, "action", nil)
@@ -228,7 +228,7 @@ func Test_loadParameters_requiredButDoesNotApply(t *testing.T) {
 	r := NewTestPorter(t)
 	defer r.Close()
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		Definitions: definition.Definitions{
 			"foo": &definition.Schema{
 				Type: "string",
@@ -243,7 +243,7 @@ func Test_loadParameters_requiredButDoesNotApply(t *testing.T) {
 				Required: true,
 			},
 		},
-	}}
+	})
 
 	i := storage.Installation{}
 	params, err := r.resolveParameters(context.Background(), i, b, "action", nil)
@@ -260,7 +260,7 @@ func Test_loadParameters_fileParameter(t *testing.T) {
 
 	r.TestConfig.TestContext.AddTestFile("testdata/file-param", "/path/to/file")
 
-	b := cnab.ExtendedBundle{bundle.Bundle{
+	b := cnab.NewBundle(bundle.Bundle{
 		RequiredExtensions: []string{
 			cnab.FileParameterExtensionKey,
 		},
@@ -279,7 +279,7 @@ func Test_loadParameters_fileParameter(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 
 	overrides := map[string]string{
 		"foo": "/path/to/file",
@@ -527,7 +527,7 @@ func Test_Paramapalooza(t *testing.T) {
 					r := NewTestPorter(t)
 					defer r.Close()
 
-					bun := cnab.ExtendedBundle{bundle.Bundle{
+					bun := cnab.NewBundle(bundle.Bundle{
 						Name:          "mybuns",
 						Version:       "1.0.0",
 						SchemaVersion: "v1.0.0",
@@ -558,7 +558,7 @@ func Test_Paramapalooza(t *testing.T) {
 								},
 							},
 						},
-					}}
+					})
 
 					if tc.DefaultExists {
 						bun.Definitions["my-param"].Default = "my-param-default"
diff --git a/pkg/porter/publish_test.go b/pkg/porter/publish_test.go
index 0596fb52..1a23570f 100644
--- a/pkg/porter/publish_test.go
+++ b/pkg/porter/publish_test.go
@@ -149,7 +149,7 @@ func TestPublish_UpdateBundleWithNewImage(t *testing.T) {
 	p := NewTestPorter(t)
 	defer p.Close()
 
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Name: "mybuns",
 		InvocationImages: []bundle.InvocationImage{
 			{
@@ -167,7 +167,7 @@ func TestPublish_UpdateBundleWithNewImage(t *testing.T) {
 				},
 			},
 		},
-	}}
+	})
 	tag := "myneworg/mynewbuns"
 
 	digest, err := image.NewDigest("sha256:6b5a28ccbb76f12ce771a23757880c6083234255c5ba191fca1c5db1f71c1687")
@@ -198,7 +198,7 @@ func TestPublish_RefreshCachedBundle(t *testing.T) {
 
 	bundleRef := cnab.BundleReference{
 		Reference:  cnab.MustParseOCIReference("myreg/mybuns"),
-		Definition: cnab.ExtendedBundle{bundle.Bundle{Name: "myreg/mybuns"}},
+		Definition: cnab.NewBundle(bundle.Bundle{Name: "myreg/mybuns"}),
 	}
 
 	// No-Op; bundle does not yet exist in cache
@@ -234,7 +234,7 @@ func TestPublish_RefreshCachedBundle_OnlyWarning(t *testing.T) {
 
 	bundleRef := cnab.BundleReference{
 		Reference:  cnab.MustParseOCIReference("myreg/mybuns"),
-		Definition: cnab.ExtendedBundle{bundle.Bundle{Name: "myreg/mybuns"}},
+		Definition: cnab.NewBundle(bundle.Bundle{Name: "myreg/mybuns"}),
 	}
 
 	p.TestCache.FindBundleMock = func(ref cnab.OCIReference) (cachedBundle cache.CachedBundle, found bool, err error) {
diff --git a/pkg/porter/show_test.go b/pkg/porter/show_test.go
index 1a94d40a..2759a4f8 100644
--- a/pkg/porter/show_test.go
+++ b/pkg/porter/show_test.go
@@ -83,7 +83,7 @@ func TestPorter_ShowInstallationWithBundle(t *testing.T) {
 				},
 			}
 
-			bun := cnab.ExtendedBundle{b}
+			bun := cnab.NewBundle(b)
 			i := p.TestInstallations.CreateInstallation(storage.NewInstallation("dev", "mywordpress"), p.TestInstallations.SetMutableInstallationValues, func(i *storage.Installation) {
 				if tc.ref != "" {
 					i.TrackBundle(cnab.MustParseOCIReference(tc.ref))
@@ -198,7 +198,7 @@ func TestPorter_ShowInstallationWithoutRecordedRun(t *testing.T) {
 		},
 	}
 
-	bun := cnab.ExtendedBundle{b}
+	bun := cnab.NewBundle(b)
 	p.TestInstallations.CreateInstallation(storage.NewInstallation("dev", "mywordpress"), p.TestInstallations.SetMutableInstallationValues, func(i *storage.Installation) {
 		i.TrackBundle(cnab.MustParseOCIReference("getporter/wordpress:v0.1.0"))
 		i.Labels = map[string]string{
diff --git a/pkg/porter/testdata/show/expected-output.yaml b/pkg/porter/testdata/show/expected-output.yaml
index 1586f690..bffef642 100644
--- a/pkg/porter/testdata/show/expected-output.yaml
+++ b/pkg/porter/testdata/show/expected-output.yaml
@@ -27,7 +27,7 @@ status:
   bundleVersion: 0.1.0
   bundleDigest: sha256:88d68ef0bdb9cedc6da3a8e341a33e5d2f8bb19d0cf7ec3f1060d3f9eb73cae9
 _calculated:
-  resolvedparameters:
+  resolvedParameters:
     - name: logLevel
       type: integer
       sensitive: false
diff --git a/pkg/runtime/runtime-manifest_test.go b/pkg/runtime/runtime-manifest_test.go
index 9b86d0f6..57f30357 100644
--- a/pkg/runtime/runtime-manifest_test.go
+++ b/pkg/runtime/runtime-manifest_test.go
@@ -140,11 +140,11 @@ func TestDependencyMetadataAvailableForTemplating(t *testing.T) {
 	require.NoError(t, err, "LoadManifestFrom failed")
 	rm := NewRuntimeManifest(c.Context, cnab.ActionInstall, m)
 	rm.bundles = map[string]cnab.ExtendedBundle{
-		"mysql": {bundle.Bundle{
+		"mysql": cnab.NewBundle(bundle.Bundle{
 			Name:        "Azure MySQL",
 			Description: "Azure MySQL database as a service",
 			Version:     "v1.0.0",
-		}},
+		}),
 	}
 
 	before, _ := yaml.Marshal(m.Install[0])
@@ -340,15 +340,15 @@ func TestResolveStep_DependencyOutput(t *testing.T) {
 	ps := cnab.ParameterSources{}
 	ps.SetParameterFromDependencyOutput("porter-mysql-password", "mysql", "password")
 	ps.SetParameterFromDependencyOutput("porter-mysql-root-password", "mysql", "root-password")
-	rm.bundle = cnab.ExtendedBundle{bundle.Bundle{
+	rm.bundle = cnab.NewBundle(bundle.Bundle{
 		Custom: map[string]interface{}{
 			cnab.ParameterSourcesExtensionKey: ps,
 		},
 		RequiredExtensions: []string{cnab.ParameterSourcesExtensionKey},
-	}}
+	})
 
 	rm.bundles = map[string]cnab.ExtendedBundle{
-		"mysql": {bundle.Bundle{
+		"mysql": cnab.NewBundle(bundle.Bundle{
 			Outputs: map[string]bundle.Output{
 				"password": {
 					Definition: "password",
@@ -361,7 +361,7 @@ func TestResolveStep_DependencyOutput(t *testing.T) {
 				"password":      {WriteOnly: makeBoolPtr(true)},
 				"root-password": {WriteOnly: makeBoolPtr(true)},
 			},
-		}},
+		}),
 	}
 
 	s := &manifest.Step{
@@ -858,11 +858,11 @@ func TestResolveImageWithUpdatedBundle(t *testing.T) {
 	img := bundle.Image{}
 	img.Image = "blah/ghost:latest"
 	img.Digest = "sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041"
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Images: map[string]bundle.Image{
 			"machine": img,
 		},
-	}}
+	})
 
 	reloMap := relocation.ImageRelocationMap{}
 
@@ -888,11 +888,11 @@ func TestResolveImageWithUpdatedMismatchedBundle(t *testing.T) {
 	img := bundle.Image{}
 	img.Image = "blah/ghost:latest"
 	img.Digest = "sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041"
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Images: map[string]bundle.Image{
 			"ghost": img,
 		},
-	}}
+	})
 
 	reloMap := relocation.ImageRelocationMap{}
 
@@ -918,11 +918,11 @@ func TestResolveImageWithRelo(t *testing.T) {
 	img := bundle.Image{}
 	img.Image = "gabrtv/microservice@sha256:cca460afa270d4c527981ef9ca4989346c56cf9b20217dcea37df1ece8120687"
 	img.Digest = "sha256:cca460afa270d4c527981ef9ca4989346c56cf9b20217dcea37df1ece8120687"
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Images: map[string]bundle.Image{
 			"machine": img,
 		},
-	}}
+	})
 
 	reloMap := relocation.ImageRelocationMap{
 		"gabrtv/microservice@sha256:cca460afa270d4c527981ef9ca4989346c56cf9b20217dcea37df1ece8120687": "my.registry/microservice@sha256:cca460afa270d4c527981ef9ca4989346c56cf9b20217dcea37df1ece8120687",
@@ -950,11 +950,11 @@ func TestResolveImageRelocationNoMatch(t *testing.T) {
 	img := bundle.Image{}
 	img.Image = "deislabs/ghost:latest"
 	img.Digest = "sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041"
-	bun := cnab.ExtendedBundle{bundle.Bundle{
+	bun := cnab.NewBundle(bundle.Bundle{
 		Images: map[string]bundle.Image{
 			"machine": img,
 		},
-	}}
+	})
 
 	reloMap := relocation.ImageRelocationMap{
 		"deislabs/nogood:latest": "cnabio/ghost:latest",
diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go
index eba88d98..d1bb1410 100644
--- a/pkg/runtime/runtime_test.go
+++ b/pkg/runtime/runtime_test.go
@@ -258,7 +258,7 @@ func TestRuntimeManifest_ApplyUnboundBundleOutputs_File(t *testing.T) {
 				},
 			}
 			rm := NewRuntimeManifest(c.Context, cnab.ActionInstall, m)
-			rm.bundle = cnab.ExtendedBundle{bundle.Bundle{
+			rm.bundle = cnab.NewBundle(bundle.Bundle{
 				Definitions: map[string]*definition.Schema{
 					tc.def.Name: &tc.def.Schema,
 				},
@@ -268,7 +268,7 @@ func TestRuntimeManifest_ApplyUnboundBundleOutputs_File(t *testing.T) {
 						Path:       tc.def.Path,
 					},
 				},
-			}}
+			})
 
 			_, err := rm.FileSystem.Create(srcPath)
 			require.NoError(t, err)
diff --git a/pkg/storage/credential_store_test.go b/pkg/storage/credential_store_test.go
index 9ee62abc..0336ad91 100644
--- a/pkg/storage/credential_store_test.go
+++ b/pkg/storage/credential_store_test.go
@@ -20,17 +20,17 @@ func TestCredentialStorage_CRUD(t *testing.T) {
 
 	require.NoError(t, cp.InsertCredentialSet(context.Background(), cs))
 
-	creds, err := cp.ListCredentialSets(context.Background(), "dev", "", nil)
+	creds, err := cp.ListCredentialSets(context.Background(), ListOptions{Namespace: "dev"})
 	require.NoError(t, err)
 	require.Len(t, creds, 1, "expected 1 credential set")
-	require.Equal(t, cs.Name, creds[0].Name, "expected to retrieve secreks credentials")
-	require.Equal(t, cs.Namespace, creds[0].Namespace, "expected to retrieve secreks credentials")
+	require.Equal(t, cs.Name, creds[0].Name, "expected to retrieve sekrets credentials")
+	require.Equal(t, cs.Namespace, creds[0].Namespace, "expected to retrieve sekrets credentials")
 
-	creds, err = cp.ListCredentialSets(context.Background(), "", "", nil)
+	creds, err = cp.ListCredentialSets(context.Background(), ListOptions{})
 	require.NoError(t, err)
 	require.Len(t, creds, 0, "expected no global credential sets")
 
-	creds, err = cp.ListCredentialSets(context.Background(), "*", "", nil)
+	creds, err = cp.ListCredentialSets(context.Background(), ListOptions{Namespace: "*"})
 	require.NoError(t, err)
 	require.Len(t, creds, 1, "expected 1 credential set defined in all namespaces")
 
@@ -45,9 +45,30 @@ func TestCredentialStorage_CRUD(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, cs.Credentials, 2)
 
+	cs2 := NewCredentialSet("dev", "sekrets-2", secrets.Strategy{
+		Name: "password-2", Source: secrets.Source{
+			Key:   "secret-2",
+			Value: "dbPassword-2"}})
+	require.NoError(t, cp.InsertCredentialSet(context.Background(), cs2))
+
+	creds, err = cp.ListCredentialSets(context.Background(), ListOptions{Namespace: "dev", Skip: 1})
+	require.NoError(t, err)
+	require.Len(t, creds, 1, "expected 1 credential set")
+	require.Equal(t, cs2.Name, creds[0].Name, "expected to retrieve sekrets-2 credentials")
+	require.Equal(t, cs2.Namespace, creds[0].Namespace, "expected to retrieve sekrets-2 credentials")
+
+	creds, err = cp.ListCredentialSets(context.Background(), ListOptions{Namespace: "dev", Limit: 1})
+	require.NoError(t, err)
+	require.Len(t, creds, 1, "expected 1 credential set")
+	require.Equal(t, cs.Name, creds[0].Name, "expected to retrieve sekrets credentials")
+	require.Equal(t, cs.Namespace, creds[0].Namespace, "expected to retrieve sekrets credentials")
+
 	require.NoError(t, cp.RemoveCredentialSet(context.Background(), cs.Namespace, cs.Name))
+	require.NoError(t, cp.RemoveCredentialSet(context.Background(), cs2.Namespace, cs2.Name))
 	_, err = cp.GetCredentialSet(context.Background(), cs.Namespace, cs.Name)
 	require.ErrorIs(t, err, ErrNotFound{})
+	_, err = cp.GetCredentialSet(context.Background(), cs2.Namespace, cs2.Name)
+	require.ErrorIs(t, err, ErrNotFound{})
 }
 
 func TestCredentialStorage_Validate_GoodSources(t *testing.T) {
diff --git a/pkg/storage/grpc_test.go b/pkg/storage/grpc_test.go
index 951e3d6f..575bacab 100644
--- a/pkg/storage/grpc_test.go
+++ b/pkg/storage/grpc_test.go
@@ -41,7 +41,7 @@ func TestRoundTripDataOverGRPC(t *testing.T) {
 	// Add an index to support filtering
 	const collection = "things"
 	err = client.EnsureIndex(ctx, plugins.EnsureIndexOptions{Indices: []plugins.Index{
-		{Collection: collection, Keys: bson.D{{"namespace", 1}, {"name", 1}}},
+		{Collection: collection, Keys: bson.D{{Key: "namespace", Value: 1}, {Key: "name", Value: 1}}},
 	}})
 	require.NoError(t, err)
 
@@ -58,7 +58,7 @@ func TestRoundTripDataOverGRPC(t *testing.T) {
 	results, err := client.Find(ctx, plugins.FindOptions{
 		Collection: collection,
 		Filter:     bson.M{"namespace": "dev"},
-		Select:     bson.D{{"name", 1}, {"namespace", 1}, {"_id", 0}},
+		Select:     bson.D{{Key: "name", Value: 1}, {Key: "namespace", Value: 1}, {Key: "_id", Value: 0}},
 	})
 	require.NoError(t, err)
 	require.Len(t, results, 1)
@@ -70,11 +70,11 @@ func TestRoundTripDataOverGRPC(t *testing.T) {
 	opts := plugins.EnsureIndexOptions{
 		Indices: []plugins.Index{
 			// query most recent outputs by run (porter installation run show, when we list outputs)
-			{Collection: CollectionOutputs, Keys: bson.D{{"namespace", 1}, {"installation", 1}, {"-resultId", 1}}},
+			{Collection: CollectionOutputs, Keys: bson.D{{Key: "namespace", Value: 1}, {Key: "installation", Value: 1}, {Key: "-resultId", Value: 1}}},
 			// query outputs by result (list)
-			{Collection: CollectionOutputs, Keys: bson.D{{"resultId", 1}, {"name", 1}}, Unique: true},
+			{Collection: CollectionOutputs, Keys: bson.D{{Key: "resultId", Value: 1}, {Key: "name", Value: 1}}, Unique: true},
 			// query most recent outputs by name for an installation
-			{Collection: CollectionOutputs, Keys: bson.D{{"namespace", 1}, {"installation", 1}, {"name", 1}, {"-resultId", 1}}},
+			{Collection: CollectionOutputs, Keys: bson.D{{Key: "namespace", Value: 1}, {Key: "installation", Value: 1}, {Key: "name", Value: 1}, {Key: "-resultId", Value: 1}}},
 		},
 	}
 
@@ -94,21 +94,21 @@ func TestRoundTripDataOverGRPC(t *testing.T) {
 		Collection: CollectionOutputs,
 		Pipeline: []bson.D{
 			// List outputs by installation
-			{{"$match", bson.M{
+			{{Key: "$match", Value: bson.M{
 				"namespace":    "dev",
 				"installation": "test",
 			}}},
 			// Reverse sort them (newest on top)
-			{{"$sort", bson.D{
-				{"namespace", 1},
-				{"installation", 1},
-				{"name", 1},
-				{"resultId", -1},
+			{{Key: "$sort", Value: bson.D{
+				{Key: "namespace", Value: 1},
+				{Key: "installation", Value: 1},
+				{Key: "name", Value: 1},
+				{Key: "resultId", Value: -1},
 			}}},
 			// Group them by output name and select the last value for each output
-			{{"$group", bson.D{
-				{"_id", "$name"},
-				{"lastOutput", bson.M{"$first": "$$ROOT"}},
+			{{Key: "$group", Value: bson.D{
+				{Key: "_id", Value: "$name"},
+				{Key: "lastOutput", Value: bson.M{"$first": "$$ROOT"}},
 			}}},
 		},
 	})
diff --git a/pkg/storage/installation_store_test.go b/pkg/storage/installation_store_test.go
index 3a0c5f9d..58b4c8f8 100644
--- a/pkg/storage/installation_store_test.go
+++ b/pkg/storage/installation_store_test.go
@@ -165,7 +165,7 @@ func TestInstallationStorageProvider_Installations(t *testing.T) {
 	defer cp.Close()
 
 	t.Run("ListInstallations", func(t *testing.T) {
-		installations, err := cp.ListInstallations(context.Background(), "dev", "", nil)
+		installations, err := cp.ListInstallations(context.Background(), ListOptions{Namespace: "dev"})
 		require.NoError(t, err, "ListInstallations failed")
 
 		require.Len(t, installations, 3, "Expected 3 installations")
@@ -183,6 +183,36 @@ func TestInstallationStorageProvider_Installations(t *testing.T) {
 		assert.Equal(t, cnab.StatusSucceeded, foo.Status.ResultStatus)
 	})
 
+	t.Run("ListInstallations with skip option", func(t *testing.T) {
+		installations, err := cp.ListInstallations(context.Background(), ListOptions{Namespace: "dev", Skip: 1})
+		require.NoError(t, err, "ListInstallations failed")
+
+		require.Len(t, installations, 2, "Expected 2 installations")
+
+		bar := installations[0]
+		assert.Equal(t, "baz", bar.Name)
+		assert.Equal(t, cnab.StatusRunning, bar.Status.ResultStatus)
+
+		baz := installations[1]
+		assert.Equal(t, "foo", baz.Name)
+		assert.Equal(t, cnab.StatusSucceeded, baz.Status.ResultStatus)
+	})
+
+	t.Run("ListInstallations with limit option", func(t *testing.T) {
+		installations, err := cp.ListInstallations(context.Background(), ListOptions{Namespace: "dev", Limit: 2})
+		require.NoError(t, err, "ListInstallations failed")
+
+		require.Len(t, installations, 2, "Expected 2 installations")
+
+		bar := installations[0]
+		assert.Equal(t, "bar", bar.Name)
+		assert.Equal(t, cnab.StatusSucceeded, bar.Status.ResultStatus)
+
+		baz := installations[1]
+		assert.Equal(t, "baz", baz.Name)
+		assert.Equal(t, cnab.StatusRunning, baz.Status.ResultStatus)
+	})
+
 	t.Run("FindInstallations by label", func(t *testing.T) {
 		opts := FindOptions{
 			Filter: map[string]interface{}{
@@ -199,7 +229,7 @@ func TestInstallationStorageProvider_Installations(t *testing.T) {
 
 	t.Run("FindInstallations - project results", func(t *testing.T) {
 		opts := FindOptions{
-			Select: bson.D{{"labels", false}},
+			Select: bson.D{{Key: "labels", Value: false}},
 			Sort:   []string{"-id"},
 			Filter: bson.M{
 				"labels.team": "red",
@@ -236,14 +266,14 @@ func TestInstallationStorageProvider_DeleteInstallation(t *testing.T) {
 	cp := generateInstallationData(t)
 	defer cp.Close()
 
-	installations, err := cp.ListInstallations(context.Background(), "dev", "", nil)
+	installations, err := cp.ListInstallations(context.Background(), ListOptions{Namespace: "dev"})
 	require.NoError(t, err, "ListInstallations failed")
 	assert.Len(t, installations, 3, "expected 3 installations")
 
 	err = cp.RemoveInstallation(context.Background(), "dev", "foo")
 	require.NoError(t, err, "RemoveInstallation failed")
 
-	installations, err = cp.ListInstallations(context.Background(), "dev", "", nil)
+	installations, err = cp.ListInstallations(context.Background(), ListOptions{Namespace: "dev"})
 	require.NoError(t, err, "ListInstallations failed")
 	assert.Len(t, installations, 2, "expected foo to be deleted")
 
diff --git a/pkg/storage/migrations/manager_test.go b/pkg/storage/migrations/manager_test.go
index 52830e1c..e559ad27 100644
--- a/pkg/storage/migrations/manager_test.go
+++ b/pkg/storage/migrations/manager_test.go
@@ -61,15 +61,15 @@ func TestManager_NoMigrationEmptyHome(t *testing.T) {
 	defer mgr.Close()
 	claimStore := storage.NewInstallationStore(mgr)
 
-	_, err := claimStore.ListInstallations(context.Background(), "", "", nil)
+	_, err := claimStore.ListInstallations(context.Background(), storage.ListOptions{})
 	require.NoError(t, err, "ListInstallations failed")
 
 	credStore := storage.NewCredentialStore(mgr, nil)
-	_, err = credStore.ListCredentialSets(context.Background(), "", "", nil)
+	_, err = credStore.ListCredentialSets(context.Background(), storage.ListOptions{})
 	require.NoError(t, err, "List credentials failed")
 
 	paramStore := storage.NewParameterStore(mgr, nil)
-	_, err = paramStore.ListParameterSets(context.Background(), "", "", nil)
+	_, err = paramStore.ListParameterSets(context.Background(), storage.ListOptions{})
 	require.NoError(t, err, "List credentials failed")
 }
 
@@ -101,7 +101,7 @@ storage.Schema{ID:"schema", Installations:"needs-migration", Credentials:"1.0.1"
 	}
 
 	t.Run("list", func(t *testing.T) {
-		_, err = claimStore.ListInstallations(context.Background(), "", "", nil)
+		_, err = claimStore.ListInstallations(context.Background(), storage.ListOptions{})
 		checkMigrationError(t, err)
 	})
 
@@ -124,7 +124,7 @@ func TestClaimStorage_NoMigrationRequiredForEmptyHome(t *testing.T) {
 	defer mgr.Close()
 	claimStore := storage.NewInstallationStore(mgr)
 
-	names, err := claimStore.ListInstallations(context.Background(), "", "", nil)
+	names, err := claimStore.ListInstallations(context.Background(), storage.ListOptions{})
 	require.NoError(t, err, "ListInstallations failed")
 	assert.Empty(t, names, "Expected an empty list of installations since porter home is new")
 }
@@ -156,7 +156,7 @@ storage.Schema{ID:"schema", Installations:"1.0.1", Credentials:"needs-migration"
 	}
 
 	t.Run("list", func(t *testing.T) {
-		_, err = credStore.ListCredentialSets(context.Background(), "", "", nil)
+		_, err = credStore.ListCredentialSets(context.Background(), storage.ListOptions{})
 		checkMigrationError(t, err)
 	})
 
@@ -177,7 +177,13 @@ func TestCredentialStorage_NoMigrationRequiredForEmptyHome(t *testing.T) {
 	testSecrets := secrets.NewTestSecretsProvider()
 	credStore := storage.NewTestCredentialProviderFor(t, mgr, testSecrets)
 
-	names, err := credStore.ListCredentialSets(context.Background(), "", "", nil)
+	names, err := credStore.ListCredentialSets(context.Background(), storage.ListOptions{
+		Namespace: "",
+		Name:      "",
+		Labels:    nil,
+		Skip:      0,
+		Limit:     0,
+	})
 	require.NoError(t, err, "List failed")
 	assert.Empty(t, names, "Expected an empty list of credentials since porter home is new")
 }
@@ -209,7 +215,7 @@ storage.Schema{ID:"schema", Installations:"1.0.1", Credentials:"1.0.1", Paramete
 	}
 
 	t.Run("list", func(t *testing.T) {
-		_, err = paramStore.ListParameterSets(context.Background(), "", "", nil)
+		_, err = paramStore.ListParameterSets(context.Background(), storage.ListOptions{})
 		checkMigrationError(t, err)
 	})
 
@@ -230,7 +236,7 @@ func TestParameterStorage_NoMigrationRequiredForEmptyHome(t *testing.T) {
 	testSecrets := secrets.NewTestSecretsProvider()
 	paramStore := storage.NewTestParameterProviderFor(t, mgr, testSecrets)
 
-	names, err := paramStore.ListParameterSets(context.Background(), "", "", nil)
+	names, err := paramStore.ListParameterSets(context.Background(), storage.ListOptions{})
 	require.NoError(t, err, "List failed")
 	assert.Empty(t, names, "Expected an empty list of parameters since porter home is new")
 }
diff --git a/pkg/storage/parameter_store_test.go b/pkg/storage/parameter_store_test.go
index 4f75eb53..055390a6 100644
--- a/pkg/storage/parameter_store_test.go
+++ b/pkg/storage/parameter_store_test.go
@@ -14,7 +14,7 @@ func TestParameterStore_CRUD(t *testing.T) {
 	defer paramStore.Close()
 
 	ctx := context.Background()
-	params, err := paramStore.ListParameterSets(ctx, "dev", "", nil)
+	params, err := paramStore.ListParameterSets(ctx, ListOptions{Namespace: "dev"})
 	require.NoError(t, err)
 	require.Empty(t, params, "Find should return no entries")
 
@@ -32,16 +32,16 @@ func TestParameterStore_CRUD(t *testing.T) {
 	err = paramStore.InsertParameterSet(ctx, myParamSet)
 	require.NoError(t, err, "Insert should successfully save")
 
-	params, err = paramStore.ListParameterSets(ctx, "dev", "", nil)
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{Namespace: "dev"})
 	require.NoError(t, err)
 	require.Len(t, params, 1, "expected 1 parameter set")
-	require.Equal(t, myParamSet.Name, params[0].Name, "expected to retrieve myparams")
+	require.Equal(t, myParamSet.Name, params[0].Name, "expected to retrieve myparam")
 
-	params, err = paramStore.ListParameterSets(ctx, "", "", nil)
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{})
 	require.NoError(t, err)
 	require.Len(t, params, 0, "expected no global parameter sets")
 
-	params, err = paramStore.ListParameterSets(ctx, "*", "", nil)
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{Namespace: "*"})
 	require.NoError(t, err)
 	require.Len(t, params, 1, "expected 1 parameter set defined in all namespaces")
 
@@ -49,16 +49,45 @@ func TestParameterStore_CRUD(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, myParamSet, pset, "Get should return the saved parameter set")
 
+	myParamSet2 := NewParameterSet("dev", "myparams2",
+		secrets.Strategy{
+			Name: "myparam2",
+			Source: secrets.Source{
+				Key:   "value2",
+				Value: "myparamvalue2",
+			},
+		})
+	myParamSet2.Status.Created = time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)
+	myParamSet2.Status.Modified = myParamSet2.Status.Created
+
+	err = paramStore.InsertParameterSet(ctx, myParamSet2)
+	require.NoError(t, err, "Insert should successfully save")
+
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{Namespace: "dev", Skip: 1})
+	require.NoError(t, err)
+	require.Len(t, params, 1, "expected 1 parameter set")
+	require.Equal(t, myParamSet2.Name, params[0].Name, "expected to retrieve myparam2")
+
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{Namespace: "dev", Limit: 1})
+	require.NoError(t, err)
+	require.Len(t, params, 1, "expected 1 parameter set")
+	require.Equal(t, myParamSet.Name, params[0].Name, "expected to retrieve myparam")
+
 	err = paramStore.RemoveParameterSet(ctx, myParamSet.Namespace, myParamSet.Name)
 	require.NoError(t, err, "Remove should successfully delete the parameter set")
 
-	params, err = paramStore.ListParameterSets(ctx, "dev", "", nil)
+	err = paramStore.RemoveParameterSet(ctx, myParamSet2.Namespace, myParamSet2.Name)
+	require.NoError(t, err, "Remove should successfully delete the parameter set")
+
+	params, err = paramStore.ListParameterSets(ctx, ListOptions{Namespace: "dev"})
 	require.NoError(t, err)
 	require.Empty(t, params, "List should return no entries")
 
 	pset, err = paramStore.GetParameterSet(ctx, "", myParamSet.Name)
 	require.ErrorIs(t, err, ErrNotFound{})
 
+	pset, err = paramStore.GetParameterSet(ctx, "", myParamSet2.Name)
+	require.ErrorIs(t, err, ErrNotFound{})
 }
 
 func TestParameterStorage_ResolveAll(t *testing.T) {
diff --git a/pkg/storage/pluginstore/grpc_test.go b/pkg/storage/pluginstore/grpc_test.go
index d4bb775a..22e53094 100644
--- a/pkg/storage/pluginstore/grpc_test.go
+++ b/pkg/storage/pluginstore/grpc_test.go
@@ -64,9 +64,9 @@ func TestConvertBsonM(t *testing.T) {
 
 func TestConvertBsonD(t *testing.T) {
 	src := bson.D{
-		{"a", "1"},
-		{"b", bson.D{
-			{"c", 1},
+		{Key: "a", Value: "1"},
+		{Key: "b", Value: bson.D{
+			{Key: "c", Value: 1},
 		}},
 	}
 
@@ -74,8 +74,8 @@ func TestConvertBsonD(t *testing.T) {
 	dest := AsOrderedMap(tmp, ConvertSliceToBsonD)
 
 	wantDest := bson.D{
-		{"a", "1"},
-		{"b", bson.D{{"c", int64(1)}}},
+		{Key: "a", Value: "1"},
+		{Key: "b", Value: bson.D{{Key: "c", Value: int64(1)}}},
 	}
 	require.Equal(t, wantDest, dest)
 }
diff --git a/pkg/storage/query_test.go b/pkg/storage/query_test.go
index 8c93646b..701b4468 100644
--- a/pkg/storage/query_test.go
+++ b/pkg/storage/query_test.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/primitive"
 )
 
 var _ Document = TestDocument{}
@@ -68,7 +69,31 @@ func TestFindOptions_ToPluginOptions(t *testing.T) {
 	}
 	po := so.ToPluginOptions("mythings")
 	wantSortDoc := bson.D{
-		{"_id", -1},
-		{"name", 1}}
+		{Key: "_id", Value: -1},
+		{Key: "name", Value: 1}}
 	require.Equal(t, wantSortDoc, po.Sort)
 }
+
+func TestListOptions_ToFindOptions(t *testing.T) {
+	opts := ListOptions{
+		Namespace: "dev",
+		Name:      "name",
+		Labels:    map[string]string{"key": "value"},
+		Skip:      1,
+		Limit:     1,
+	}
+
+	wantOpts := FindOptions{
+		Sort:  []string{"namespace", "name"},
+		Skip:  1,
+		Limit: 1,
+		Filter: primitive.M{
+			"labels.key": "value",
+			"name":       map[string]interface{}{"$regex": "name"},
+			"namespace":  "dev",
+		},
+	}
+
+	gotOpts := opts.ToFindOptions()
+	require.Equal(t, wantOpts, gotOpts)
+}