Repository: helm/helm
Pull request #9782 (2021-06-08 06:29:46)

Implement changes proposed in HIP 6: OCI Support
For more information, please see the following URL: https://github.com/helm/community/blob/main/hips/hip-0006.md

Note: OCI support remains experimental, and you are still required to set HELM_EXPERIMENTAL_OCI=1 in your environment.

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

This PR implements the changes outlined in HIP 6. This will provide a more stable foundation for Helm's OCI registry support.

This adds a new `helm push` subcommand and `Pusher` interface for uploading charts, which can be expanded to other non-OCI protocols and various uploader plugins in the future (equivalent of `helm pull` subcommand and `Getter` interface).

This removes the experimental `helm chart` subcommand, and any notion of an OCI cache has been removed for the time being. The `helm registry` subcommand remains as is.

Throughout the rest of the code, OCI-based charts are now supported. For example, in `helm install`:
```
helm install oci://example.com/charts/nginx --version 0.1.0
```

Support for provenance files has been added, which are uploaded as a second layer on the OCI image manifest. These can be uploaded ~~using the `--with-prov` flag on `helm push`~~ if an associated `.prov` file is present in the same directory.

Chart references are strictly enforced (for now) to follow the following pattern:
```
<host>/<namespace>/<chart_name>:<chart_version>
```

Tags such as "latest" or "stable" are not yet supported.

Lastly, the `internal/experimental/registry` package has been stabilized. The public-facing `Login`, `Logout`, `Pull`, and `Push` methods all follow a common pattern, taking options and returning a result object.

Once the Go API is made public, for users wishing to upload charts using arbitrary tags or basenames, the `PushOptStrictMode(false)` option is available to enable this behavior.

**Special notes for your reviewer**:

All of the changes under `pkg/pusher/` have been based on `pkg/getter/`, and all of the changes under `pkg/uploader/` have been based on `pkg/downloader/`. An attempt was made to follow existing patterns wherever possible.

An initial version of this changeset included support for "uploader" plugins, but it has since been removed due to the fact that this change is already very large. The framework is in place to add support for this later.

Since these changes operate against remote registries, a new test suite in helm/acceptance-testing has been created (not yet merged). This test suite has been used to test these changes throughout the development process.

The test suite verifies that a compiled version of Helm performs as expected when operating against a live OCI registry. The test suite source can be found here: https://github.com/bloodorangeio/acceptance-testing/blob/hip-6-push/testsuites/registries.robot

Documentation has been added on Go methods, structs, etc., but not yet on helm/helm-www. Once these changes are merged, documentation will be addressed further.

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility



Patch:
diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go
deleted file mode 100644
index adc874cfe..000000000
--- a/cmd/helm/chart.go
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
-Copyright The Helm Authors.
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartHelp = `
-This command consists of multiple subcommands to work with the chart cache.
-
-The subcommands can be used to push, pull, tag, list, or remove Helm charts.
-`
-
-func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:               "chart",
-		Short:             "push, pull, tag, or remove Helm charts",
-		Long:              chartHelp,
-		Hidden:            !FeatureGateOCI.IsEnabled(),
-		PersistentPreRunE: checkOCIFeatureGate(),
-	}
-	cmd.AddCommand(
-		newChartListCmd(cfg, out),
-		newChartExportCmd(cfg, out),
-		newChartPullCmd(cfg, out),
-		newChartPushCmd(cfg, out),
-		newChartRemoveCmd(cfg, out),
-		newChartSaveCmd(cfg, out),
-	)
-	return cmd
-}
diff --git a/cmd/helm/chart_export.go b/cmd/helm/chart_export.go
deleted file mode 100644
index 67caf08d7..000000000
--- a/cmd/helm/chart_export.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/cmd/helm/require"
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartExportDesc = `
-Export a chart stored in local registry cache.
-
-This will create a new directory with the name of
-the chart, in a format that developers can modify
-and check into source control if desired.
-`
-
-func newChartExportCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	client := action.NewChartExport(cfg)
-
-	cmd := &cobra.Command{
-		Use:    "export [ref]",
-		Short:  "export a chart to directory",
-		Long:   chartExportDesc,
-		Args:   require.MinimumNArgs(1),
-		Hidden: !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			ref := args[0]
-			return client.Run(out, ref)
-		},
-	}
-
-	f := cmd.Flags()
-	f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")
-
-	return cmd
-}
diff --git a/cmd/helm/chart_list.go b/cmd/helm/chart_list.go
deleted file mode 100644
index e81882ccc..000000000
--- a/cmd/helm/chart_list.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartListDesc = `
-List all charts in the local registry cache.
-
-Charts are sorted by ref name, alphabetically.
-`
-
-func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	chartList := action.NewChartList(cfg)
-	cmd := &cobra.Command{
-		Use:     "list",
-		Aliases: []string{"ls"},
-		Short:   "list all saved charts",
-		Long:    chartListDesc,
-		Hidden:  !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return chartList.Run(out)
-		},
-	}
-	f := cmd.Flags()
-	f.UintVar(&chartList.ColumnWidth, "max-col-width", 60, "maximum column width for output table")
-	return cmd
-}
diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go
deleted file mode 100644
index 760ff3e2c..000000000
--- a/cmd/helm/chart_pull.go
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/cmd/helm/require"
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartPullDesc = `
-Download a chart from a remote registry.
-
-This will store the chart in the local registry cache to be used later.
-`
-
-func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	return &cobra.Command{
-		Use:    "pull [ref]",
-		Short:  "pull a chart from remote",
-		Long:   chartPullDesc,
-		Args:   require.MinimumNArgs(1),
-		Hidden: !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			ref := args[0]
-			return action.NewChartPull(cfg).Run(out, ref)
-		},
-	}
-}
diff --git a/cmd/helm/chart_push.go b/cmd/helm/chart_push.go
deleted file mode 100644
index ff34632b1..000000000
--- a/cmd/helm/chart_push.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/cmd/helm/require"
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartPushDesc = `
-Upload a chart to a remote registry.
-
-Note: the ref must already exist in the local registry cache.
-
-Must first run "helm chart save" or "helm chart pull".
-`
-
-func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	return &cobra.Command{
-		Use:    "push [ref]",
-		Short:  "push a chart to remote",
-		Long:   chartPushDesc,
-		Args:   require.MinimumNArgs(1),
-		Hidden: !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			ref := args[0]
-			return action.NewChartPush(cfg).Run(out, ref)
-		},
-	}
-}
diff --git a/cmd/helm/chart_remove.go b/cmd/helm/chart_remove.go
deleted file mode 100644
index d952951fb..000000000
--- a/cmd/helm/chart_remove.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/cmd/helm/require"
-	"helm.sh/helm/v3/pkg/action"
-)
-
-const chartRemoveDesc = `
-Remove a chart from the local registry cache.
-
-Note: the chart content will still exist in the cache,
-but it will no longer appear in "helm chart list".
-
-To remove all unlinked content, please run "helm chart prune". (TODO)
-`
-
-func newChartRemoveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	return &cobra.Command{
-		Use:     "remove [ref]",
-		Aliases: []string{"rm"},
-		Short:   "remove a chart",
-		Long:    chartRemoveDesc,
-		Args:    require.MinimumNArgs(1),
-		Hidden:  !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			ref := args[0]
-			return action.NewChartRemove(cfg).Run(out, ref)
-		},
-	}
-}
diff --git a/cmd/helm/chart_save.go b/cmd/helm/chart_save.go
deleted file mode 100644
index 35b72cd07..000000000
--- a/cmd/helm/chart_save.go
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"io"
-	"path/filepath"
-
-	"github.com/spf13/cobra"
-
-	"helm.sh/helm/v3/cmd/helm/require"
-	"helm.sh/helm/v3/pkg/action"
-	"helm.sh/helm/v3/pkg/chart/loader"
-)
-
-const chartSaveDesc = `
-Store a copy of chart in local registry cache.
-
-Note: modifying the chart after this operation will
-not change the item as it exists in the cache.
-`
-
-func newChartSaveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
-	return &cobra.Command{
-		Use:    "save [path] [ref]",
-		Short:  "save a chart directory",
-		Long:   chartSaveDesc,
-		Args:   require.MinimumNArgs(2),
-		Hidden: !FeatureGateOCI.IsEnabled(),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			path := args[0]
-			ref := args[1]
-
-			path, err := filepath.Abs(path)
-			if err != nil {
-				return err
-			}
-
-			ch, err := loader.Load(path)
-			if err != nil {
-				return err
-			}
-
-			return action.NewChartSave(cfg).Run(out, ch, ref)
-		},
-	}
-}
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index 04419d730..d8bedddfe 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -182,6 +182,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
 	}
 	client.ReleaseName = name
 
+	if err := checkOCI(chart); err != nil {
+		return nil, err
+	}
+
 	cp, err := client.ChartPathOptions.LocateChart(chart, settings)
 	if err != nil {
 		return nil, err
diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go
index 7711320f1..c910007db 100644
--- a/cmd/helm/pull.go
+++ b/cmd/helm/pull.go
@@ -20,7 +20,6 @@ import (
 	"fmt"
 	"io"
 	"log"
-	"strings"
 
 	"github.com/spf13/cobra"
 
@@ -65,10 +64,8 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 				client.Version = ">0.0.0-0"
 			}
 
-			if strings.HasPrefix(args[0], "oci://") {
-				if !FeatureGateOCI.IsEnabled() {
-					return FeatureGateOCI.Error()
-				}
+			if err := checkOCI(args[0]); err != nil {
+				return err
 			}
 
 			for i := 0; i < len(args); i++ {
diff --git a/cmd/helm/push.go b/cmd/helm/push.go
new file mode 100644
index 000000000..7daa66563
--- /dev/null
+++ b/cmd/helm/push.go
@@ -0,0 +1,61 @@
+/*
+Copyright The Helm Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/spf13/cobra"
+
+	"helm.sh/helm/v3/cmd/helm/require"
+	experimental "helm.sh/helm/v3/internal/experimental/action"
+	"helm.sh/helm/v3/pkg/action"
+)
+
+const pushDesc = `
+Upload a chart to a registry.
+
+If the chart has an associated provenance file,
+it will also be uploaded.
+`
+
+func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
+	client := experimental.NewPushWithOpts(experimental.WithPushConfig(cfg))
+
+	cmd := &cobra.Command{
+		Use:               "push [chart] [remote]",
+		Short:             "push a chart to remote",
+		Long:              pushDesc,
+		Hidden:            !FeatureGateOCI.IsEnabled(),
+		PersistentPreRunE: checkOCIFeatureGate(),
+		Args:              require.MinimumNArgs(2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			chartRef := args[0]
+			remote := args[1]
+			client.Settings = settings
+			output, err := client.Run(chartRef, remote)
+			if err != nil {
+				return err
+			}
+			fmt.Fprint(out, output)
+			return nil
+		},
+	}
+
+	return cmd
+}
diff --git a/cmd/helm/registry_login.go b/cmd/helm/registry_login.go
index 8724f11f9..08c2daa3b 100644
--- a/cmd/helm/registry_login.go
+++ b/cmd/helm/registry_login.go
@@ -29,6 +29,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"helm.sh/helm/v3/cmd/helm/require"
+	experimental "helm.sh/helm/v3/internal/experimental/action"
 	"helm.sh/helm/v3/pkg/action"
 )
 
@@ -54,7 +55,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
 				return err
 			}
 
-			return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
+			return experimental.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
 		},
 	}
 
@@ -67,7 +68,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
 	return cmd
 }
 
-// Adapted from https://oras.land/oras-go
+// Adapted from https://github.com/oras-project/oras
 func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
 	var err error
 	username := usernameOpt
@@ -110,7 +111,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
 	return username, password, nil
 }
 
-// Copied/adapted from https://oras.land/oras-go
+// Copied/adapted from https://github.com/oras-project/oras
 func readLine(prompt string, silent bool) (string, error) {
 	fmt.Print(prompt)
 	if silent {
diff --git a/cmd/helm/registry_logout.go b/cmd/helm/registry_logout.go
index e7e1a24fe..abeb90461 100644
--- a/cmd/helm/registry_logout.go
+++ b/cmd/helm/registry_logout.go
@@ -22,6 +22,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"helm.sh/helm/v3/cmd/helm/require"
+	experimental "helm.sh/helm/v3/internal/experimental/action"
 	"helm.sh/helm/v3/pkg/action"
 )
 
@@ -38,7 +39,7 @@ func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
 		Hidden: !FeatureGateOCI.IsEnabled(),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			hostname := args[0]
-			return action.NewRegistryLogout(cfg).Run(out, hostname)
+			return experimental.NewRegistryLogout(cfg).Run(out, hostname)
 		},
 	}
 }
diff --git a/cmd/helm/root.go b/cmd/helm/root.go
index dbb31ad78..0de4a738a 100644
--- a/cmd/helm/root.go
+++ b/cmd/helm/root.go
@@ -200,7 +200,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
 	// Add *experimental* subcommands
 	cmd.AddCommand(
 		newRegistryCmd(actionConfig, out),
-		newChartCmd(actionConfig, out),
+		newPushCmd(actionConfig, out),
 	)
 
 	// Find and add plugins
@@ -262,3 +262,12 @@ func checkForExpiredRepos(repofile string) {
 	}
 
 }
+
+// When dealing with OCI-based charts, ensure that the user has
+// enabled the experimental feature gate prior to continuing
+func checkOCI(ref string) error {
+	if registry.IsOCI(ref) && !FeatureGateOCI.IsEnabled() {
+		return FeatureGateOCI.Error()
+	}
+	return nil
+}
diff --git a/cmd/helm/show.go b/cmd/helm/show.go
index 7c6da270c..ee56e4666 100644
--- a/cmd/helm/show.go
+++ b/cmd/helm/show.go
@@ -198,6 +198,10 @@ func runShow(args []string, client *action.Show) (string, error) {
 		client.Version = ">0.0.0-0"
 	}
 
+	if err := checkOCI(args[0]); err != nil {
+		return "", err
+	}
+
 	cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
 	if err != nil {
 		return "", err
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 3bd392d1d..e73c48bf1 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -83,6 +83,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
+			if err := checkOCI(args[1]); err != nil {
+				return err
+			}
+
 			client.Namespace = settings.Namespace()
 
 			// Fixes #7002 - Support reading values from STDIN for `upgrade` command
diff --git a/go.mod b/go.mod
index 70a0b3859..9ccc410d9 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,6 @@ require (
 	github.com/cyphar/filepath-securejoin v0.2.2
 	github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
 	github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
-	github.com/docker/go-units v0.4.0
 	github.com/evanphx/json-patch v4.11.0+incompatible
 	github.com/gobwas/glob v0.2.3
 	github.com/gofrs/flock v0.8.0
@@ -23,7 +22,6 @@ require (
 	github.com/lib/pq v1.10.0
 	github.com/mattn/go-shellwords v1.0.11
 	github.com/mitchellh/copystructure v1.1.1
-	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.0.1
 	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
 	github.com/pkg/errors v0.9.1
@@ -45,7 +43,6 @@ require (
 	k8s.io/klog/v2 v2.9.0
 	k8s.io/kubectl v0.22.1
 	oras.land/oras-go v0.4.0
+	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/yaml v1.2.0
 )
-
-replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
diff --git a/go.sum b/go.sum
index fbe908dd9..e6efd0574 100644
--- a/go.sum
+++ b/go.sum
@@ -308,8 +308,10 @@ github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3/go.mo
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/cli v20.10.7+incompatible h1:pv/3NqibQKphWZiAskMzdz8w0PRbtTaEB+f6NwdU7Is=
 github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v0.0.0-20191216044856-a8371794149d h1:jC8tT/S0OGx2cswpeUTn4gOIea8P08lD3VFQT0cOZ50=
-github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
 github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
@@ -1495,6 +1497,8 @@ k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
 oras.land/oras-go v0.4.0 h1:u6+7D+raZDYHwlz/uOwNANiRmyYDSSMW7A9E1xXycUQ=
 oras.land/oras-go v0.4.0/go.mod h1:VJcU+VE4rkclUbum5C0O7deEZbBYnsnpbGSACwTjOcg=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/letsencrypt v0.0.3 h1:H7xDfhkaFFSYEJlKeq38RwX2jYcnTeHuDQyT+mMNMwM=
+rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
diff --git a/internal/experimental/action/push.go b/internal/experimental/action/push.go
new file mode 100644
index 000000000..b125ae1f4
--- /dev/null
+++ b/internal/experimental/action/push.go
@@ -0,0 +1,71 @@
+/*
+Copyright The Helm Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+	"strings"
+
+	"helm.sh/helm/v3/internal/experimental/pusher"
+	"helm.sh/helm/v3/internal/experimental/registry"
+	"helm.sh/helm/v3/internal/experimental/uploader"
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/cli"
+)
+
+// Push is the action for uploading a chart.
+//
+// It provides the implementation of 'helm push'.
+type Push struct {
+	Settings *cli.EnvSettings
+	cfg      *action.Configuration
+}
+
+// PushOpt is a type of function that sets options for a push action.
+type PushOpt func(*Push)
+
+// WithPushConfig sets the cfg field on the push configuration object.
+func WithPushConfig(cfg *action.Configuration) PushOpt {
+	return func(p *Push) {
+		p.cfg = cfg
+	}
+}
+
+// NewPushWithOpts creates a new push, with configuration options.
+func NewPushWithOpts(opts ...PushOpt) *Push {
+	p := &Push{}
+	for _, fn := range opts {
+		fn(p)
+	}
+	return p
+}
+
+// Run executes 'helm push' against the given chart archive.
+func (p *Push) Run(chartRef string, remote string) (string, error) {
+	var out strings.Builder
+
+	c := uploader.ChartUploader{
+		Out:     &out,
+		Pushers: pusher.All(p.Settings),
+		Options: []pusher.Option{},
+	}
+
+	if registry.IsOCI(remote) {
+		c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient))
+	}
+
+	return out.String(), c.UploadTo(chartRef, remote)
+}
diff --git a/pkg/action/registry_login.go b/internal/experimental/action/registry_login.go
similarity index 75%
rename from pkg/action/registry_login.go
rename to internal/experimental/action/registry_login.go
index 00f6e2644..8312f3c57 100644
--- a/pkg/action/registry_login.go
+++ b/internal/experimental/action/registry_login.go
@@ -18,15 +18,18 @@ package action
 
 import (
 	"io"
+
+	"helm.sh/helm/v3/internal/experimental/registry"
+	"helm.sh/helm/v3/pkg/action"
 )
 
 // RegistryLogin performs a registry login operation.
 type RegistryLogin struct {
-	cfg *Configuration
+	cfg *action.Configuration
 }
 
 // NewRegistryLogin creates a new RegistryLogin object with the given configuration.
-func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
+func NewRegistryLogin(cfg *action.Configuration) *RegistryLogin {
 	return &RegistryLogin{
 		cfg: cfg,
 	}
@@ -34,5 +37,8 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
 
 // Run executes the registry login operation
 func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
-	return a.cfg.RegistryClient.Login(hostname, username, password, insecure)
+	return a.cfg.RegistryClient.Login(
+		hostname,
+		registry.LoginOptBasicAuth(username, password),
+		registry.LoginOptInsecure(insecure))
 }
diff --git a/pkg/action/registry_logout.go b/internal/experimental/action/registry_logout.go
similarity index 88%
rename from pkg/action/registry_logout.go
rename to internal/experimental/action/registry_logout.go
index 69add4163..039515c7e 100644
--- a/pkg/action/registry_logout.go
+++ b/internal/experimental/action/registry_logout.go
@@ -18,15 +18,17 @@ package action
 
 import (
 	"io"
+
+	"helm.sh/helm/v3/pkg/action"
 )
 
 // RegistryLogout performs a registry login operation.
 type RegistryLogout struct {
-	cfg *Configuration
+	cfg *action.Configuration
 }
 
 // NewRegistryLogout creates a new RegistryLogout object with the given configuration.
-func NewRegistryLogout(cfg *Configuration) *RegistryLogout {
+func NewRegistryLogout(cfg *action.Configuration) *RegistryLogout {
 	return &RegistryLogout{
 		cfg: cfg,
 	}
diff --git a/internal/experimental/registry/resolver.go b/internal/experimental/pusher/doc.go
similarity index 64%
rename from internal/experimental/registry/resolver.go
rename to internal/experimental/pusher/doc.go
index ff8a82633..df89ab112 100644
--- a/internal/experimental/registry/resolver.go
+++ b/internal/experimental/pusher/doc.go
@@ -1,11 +1,10 @@
 /*
 Copyright The Helm Authors.
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,15 +13,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"github.com/containerd/containerd/remotes"
-)
-
-type (
-	// Resolver provides remotes based on a locator
-	Resolver struct {
-		remotes.Resolver
-	}
-)
+/*
+Package pusher provides a generalized tool for uploading data by scheme.
+This provides a method by which the plugin system can load arbitrary protocol
+handlers based upon a URL scheme.
+*/
+package pusher
diff --git a/internal/experimental/pusher/ocipusher.go b/internal/experimental/pusher/ocipusher.go
new file mode 100644
index 000000000..a1df0da85
--- /dev/null
+++ b/internal/experimental/pusher/ocipusher.go
@@ -0,0 +1,104 @@
+/*
+Copyright The Helm Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pusher
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	"helm.sh/helm/v3/internal/experimental/registry"
+	"helm.sh/helm/v3/pkg/chart/loader"
+)
+
+// OCIPusher is the default OCI backend handler
+type OCIPusher struct {
+	opts options
+}
+
+// Push performs a Push from repo.Pusher.
+func (pusher *OCIPusher) Push(chartRef, href string, options ...Option) error {
+	for _, opt := range options {
+		opt(&pusher.opts)
+	}
+	return pusher.push(chartRef, href)
+}
+
+func (pusher *OCIPusher) push(chartRef, href string) error {
+	stat, err := os.Stat(chartRef)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return errors.Errorf("%s: no such file", chartRef)
+		}
+		return err
+	}
+	if stat.IsDir() {
+		return errors.New("cannot push directory, must provide chart archive (.tgz)")
+	}
+
+	meta, err := loader.Load(chartRef)
+	if err != nil {
+		return err
+	}
+
+	client := pusher.opts.registryClient
+
+	chartBytes, err := ioutil.ReadFile(chartRef)
+	if err != nil {
+		return err
+	}
+
+	var pushOpts []registry.PushOption
+	provRef := fmt.Sprintf("%s.prov", chartRef)
+	if _, err := os.Stat(provRef); err == nil {
+		provBytes, err := ioutil.ReadFile(provRef)
+		if err != nil {
+			return err
+		}
+		pushOpts = append(pushOpts, registry.PushOptProvData(provBytes))
+	}
+
+	ref := fmt.Sprintf("%s:%s",
+		path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name),
+		meta.Metadata.Version)
+
+	_, err = client.Push(chartBytes, ref, pushOpts...)
+	return err
+}
+
+// NewOCIPusher constructs a valid OCI client as a Pusher
+func NewOCIPusher(ops ...Option) (Pusher, error) {
+	registryClient, err := registry.NewClient()
+	if err != nil {
+		return nil, err
+	}
+
+	client := OCIPusher{
+		opts: options{
+			registryClient: registryClient,
+		},
+	}
+
+	for _, opt := range ops {
+		opt(&client.opts)
+	}
+
+	return &client, nil
+}
diff --git a/internal/experimental/pusher/pusher.go b/internal/experimental/pusher/pusher.go
new file mode 100644
index 000000000..32c1351e9
--- /dev/null
+++ b/internal/experimental/pusher/pusher.go
@@ -0,0 +1,95 @@
+/*
+Copyright The Helm Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pusher
+
+import (
+	"github.com/pkg/errors"
+
+	"helm.sh/helm/v3/internal/experimental/registry"
+	"helm.sh/helm/v3/pkg/cli"
+)
+
+// options are generic parameters to be provided to the pusher during instantiation.
+//
+// Pushers may or may not ignore these parameters as they are passed in.
+type options struct {
+	registryClient *registry.Client
+}
+
+// Option allows specifying various settings configurable by the user for overriding the defaults
+// used when performing Push operations with the Pusher.
+type Option func(*options)
+
+// WithRegistryClient sets the registryClient option.
+func WithRegistryClient(client *registry.Client) Option {
+	return func(opts *options) {
+		opts.registryClient = client
+	}
+}
+
+// Pusher is an interface to support upload to the specified URL.
+type Pusher interface {
+	// Push file content by url string
+	Push(chartRef, url string, options ...Option) error
+}
+
+// Constructor is the function for every pusher which creates a specific instance
+// according to the configuration
+type Constructor func(options ...Option) (Pusher, error)
+
+// Provider represents any pusher and the schemes that it supports.
+type Provider struct {
+	Schemes []string
+	New     Constructor
+}
+
+// Provides returns true if the given scheme is supported by this Provider.
+func (p Provider) Provides(scheme string) bool {
+	for _, i := range p.Schemes {
+		if i == scheme {
+			return true
+		}
+	}
+	return false
+}
+
+// Providers is a collection of Provider objects.
+type Providers []Provider
+
+// ByScheme returns a Provider that handles the given scheme.
+//
+// If no provider handles this scheme, this will return an error.
+func (p Providers) ByScheme(scheme string) (Pusher, error) {
+	for _, pp := range p {
+		if pp.Provides(scheme) {
+			return pp.New()
+		}
+	}
+	return nil, errors.Errorf("scheme %q not supported", scheme)
+}
+
+var ociProvider = Provider{
+	Schemes: []string{registry.OCIScheme},
+	New:     NewOCIPusher,
+}
+
+// All finds all of the registered pushers as a list of Provider instances.
+// Currently, just the built-in pushers are collected.
+func All(settings *cli.EnvSettings) Providers {
+	result := Providers{ociProvider}
+	return result
+}
diff --git a/internal/experimental/registry/cache.go b/internal/experimental/registry/cache.go
deleted file mode 100644
index c89898a51..000000000
--- a/internal/experimental/registry/cache.go
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"time"
-
-	"github.com/containerd/containerd/content"
-	"github.com/containerd/containerd/errdefs"
-	digest "github.com/opencontainers/go-digest"
-	specs "github.com/opencontainers/image-spec/specs-go"
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
-	"github.com/pkg/errors"
-	orascontent "oras.land/oras-go/pkg/content"
-
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/chart/loader"
-	"helm.sh/helm/v3/pkg/chartutil"
-)
-
-const (
-	// CacheRootDir is the root directory for a cache
-	CacheRootDir = "cache"
-)
-
-type (
-	// Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout
-	Cache struct {
-		debug       bool
-		out         io.Writer
-		rootDir     string
-		ociStore    *orascontent.OCIStore
-		memoryStore *orascontent.Memorystore
-	}
-
-	// CacheRefSummary contains as much info as available describing a chart reference in cache
-	// Note: fields here are sorted by the order in which they are set in FetchReference method
-	CacheRefSummary struct {
-		Name         string
-		Repo         string
-		Tag          string
-		Exists       bool
-		Manifest     *ocispec.Descriptor
-		Config       *ocispec.Descriptor
-		ContentLayer *ocispec.Descriptor
-		Size         int64
-		Digest       digest.Digest
-		CreatedAt    time.Time
-		Chart        *chart.Chart
-	}
-)
-
-// NewCache returns a new OCI Layout-compliant cache with config
-func NewCache(opts ...CacheOption) (*Cache, error) {
-	cache := &Cache{
-		out: ioutil.Discard,
-	}
-	for _, opt := range opts {
-		opt(cache)
-	}
-	// validate
-	if cache.rootDir == "" {
-		return nil, errors.New("must set cache root dir on initialization")
-	}
-	return cache, nil
-}
-
-// FetchReference retrieves a chart ref from cache
-func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) {
-	if err := cache.init(); err != nil {
-		return nil, err
-	}
-	r := CacheRefSummary{
-		Name: ref.FullName(),
-		Repo: ref.Repo,
-		Tag:  ref.Tag,
-	}
-	for _, desc := range cache.ociStore.ListReferences() {
-		if desc.Annotations[ocispec.AnnotationRefName] == r.Name {
-			r.Exists = true
-			manifestBytes, err := cache.fetchBlob(&desc)
-			if err != nil {
-				return &r, err
-			}
-			var manifest ocispec.Manifest
-			err = json.Unmarshal(manifestBytes, &manifest)
-			if err != nil {
-				return &r, err
-			}
-			r.Manifest = &desc
-			r.Config = &manifest.Config
-			numLayers := len(manifest.Layers)
-			if numLayers != 1 {
-				return &r, errors.New(
-					fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers))
-			}
-			var contentLayer *ocispec.Descriptor
-			for _, layer := range manifest.Layers {
-				switch layer.MediaType {
-				case HelmChartContentLayerMediaType:
-					contentLayer = &layer
-				}
-			}
-			if contentLayer == nil {
-				return &r, errors.New(
-					fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
-			}
-			if contentLayer.Size == 0 {
-				return &r, errors.New(
-					fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType))
-			}
-			r.ContentLayer = contentLayer
-			info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
-			if err != nil {
-				return &r, err
-			}
-			r.Size = info.Size
-			r.Digest = info.Digest
-			r.CreatedAt = info.CreatedAt
-			contentBytes, err := cache.fetchBlob(contentLayer)
-			if err != nil {
-				return &r, err
-			}
-			ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
-			if err != nil {
-				return &r, err
-			}
-			r.Chart = ch
-		}
-	}
-	return &r, nil
-}
-
-// StoreReference stores a chart ref in cache
-func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) {
-	if err := cache.init(); err != nil {
-		return nil, err
-	}
-	r := CacheRefSummary{
-		Name:  ref.FullName(),
-		Repo:  ref.Repo,
-		Tag:   ref.Tag,
-		Chart: ch,
-	}
-	existing, _ := cache.FetchReference(ref)
-	r.Exists = existing.Exists
-	config, _, err := cache.saveChartConfig(ch)
-	if err != nil {
-		return &r, err
-	}
-	r.Config = config
-	contentLayer, _, err := cache.saveChartContentLayer(ch)
-	if err != nil {
-		return &r, err
-	}
-	r.ContentLayer = contentLayer
-	info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
-	if err != nil {
-		return &r, err
-	}
-	r.Size = info.Size
-	r.Digest = info.Digest
-	r.CreatedAt = info.CreatedAt
-	manifest, _, err := cache.saveChartManifest(config, contentLayer)
-	if err != nil {
-		return &r, err
-	}
-	r.Manifest = manifest
-	return &r, nil
-}
-
-// DeleteReference deletes a chart ref from cache
-// TODO: garbage collection, only manifest removed
-func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
-	if err := cache.init(); err != nil {
-		return nil, err
-	}
-	r, err := cache.FetchReference(ref)
-	if err != nil || !r.Exists {
-		return r, err
-	}
-	cache.ociStore.DeleteReference(r.Name)
-	err = cache.ociStore.SaveIndex()
-	return r, err
-}
-
-// ListReferences lists all chart refs in a cache
-func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
-	if err := cache.init(); err != nil {
-		return nil, err
-	}
-	var rr []*CacheRefSummary
-	for _, desc := range cache.ociStore.ListReferences() {
-		name := desc.Annotations[ocispec.AnnotationRefName]
-		if name == "" {
-			if cache.debug {
-				fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex())
-			}
-			continue
-		}
-		ref, err := ParseReference(name)
-		if err != nil {
-			return rr, err
-		}
-		r, err := cache.FetchReference(ref)
-		if err != nil {
-			return rr, err
-		}
-		rr = append(rr, r)
-	}
-	return rr, nil
-}
-
-// AddManifest provides a manifest to the cache index.json
-func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
-	if err := cache.init(); err != nil {
-		return err
-	}
-	cache.ociStore.AddReference(ref.FullName(), *manifest)
-	err := cache.ociStore.SaveIndex()
-	return err
-}
-
-// Provider provides a valid containerd Provider
-func (cache *Cache) Provider() content.Provider {
-	return content.Provider(cache.ociStore)
-}
-
-// Ingester provides a valid containerd Ingester
-func (cache *Cache) Ingester() content.Ingester {
-	return content.Ingester(cache.ociStore)
-}
-
-// ProvideIngester provides a valid oras ProvideIngester
-func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
-	return orascontent.ProvideIngester(cache.ociStore)
-}
-
-// init creates files needed necessary for OCI layout store
-func (cache *Cache) init() error {
-	if cache.ociStore == nil {
-		ociStore, err := orascontent.NewOCIStore(cache.rootDir)
-		if err != nil {
-			return err
-		}
-		cache.ociStore = ociStore
-		cache.memoryStore = orascontent.NewMemoryStore()
-	}
-	return nil
-}
-
-// saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
-func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
-	configBytes, err := json.Marshal(ch.Metadata)
-	if err != nil {
-		return nil, false, err
-	}
-	configExists, err := cache.storeBlob(configBytes)
-	if err != nil {
-		return nil, configExists, err
-	}
-	descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
-	return &descriptor, configExists, nil
-}
-
-// saveChartContentLayer stores the chart as tarball blob and returns a descriptor
-func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
-	destDir := filepath.Join(cache.rootDir, ".build")
-	os.MkdirAll(destDir, 0755)
-	tmpFile, err := chartutil.Save(ch, destDir)
-	defer os.Remove(tmpFile)
-	if err != nil {
-		return nil, false, errors.Wrap(err, "failed to save")
-	}
-	contentBytes, err := ioutil.ReadFile(tmpFile)
-	if err != nil {
-		return nil, false, err
-	}
-	contentExists, err := cache.storeBlob(contentBytes)
-	if err != nil {
-		return nil, contentExists, err
-	}
-	descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes)
-	return &descriptor, contentExists, nil
-}
-
-// saveChartManifest stores the chart manifest as json blob and returns a descriptor
-func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) {
-	manifest := ocispec.Manifest{
-		Versioned: specs.Versioned{SchemaVersion: 2},
-		Config:    *config,
-		Layers:    []ocispec.Descriptor{*contentLayer},
-	}
-	manifestBytes, err := json.Marshal(manifest)
-	if err != nil {
-		return nil, false, err
-	}
-	manifestExists, err := cache.storeBlob(manifestBytes)
-	if err != nil {
-		return nil, manifestExists, err
-	}
-	descriptor := ocispec.Descriptor{
-		MediaType: ocispec.MediaTypeImageManifest,
-		Digest:    digest.FromBytes(manifestBytes),
-		Size:      int64(len(manifestBytes)),
-	}
-	return &descriptor, manifestExists, nil
-}
-
-// storeBlob stores a blob on filesystem
-func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) {
-	var exists bool
-	writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug),
-		content.WithRef(digest.FromBytes(blobBytes).Hex()))
-	if err != nil {
-		return exists, err
-	}
-	_, err = writer.Write(blobBytes)
-	if err != nil {
-		return exists, err
-	}
-	err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
-	if err != nil {
-		if !errdefs.IsAlreadyExists(err) {
-			return exists, err
-		}
-		exists = true
-	}
-	err = writer.Close()
-	return exists, err
-}
-
-// fetchBlob retrieves a blob from filesystem
-func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) {
-	reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc)
-	if err != nil {
-		return nil, err
-	}
-	defer reader.Close()
-
-	bytes := make([]byte, desc.Size)
-	_, err = reader.ReadAt(bytes, 0)
-	if err != nil {
-		return nil, err
-	}
-	return bytes, nil
-}
diff --git a/internal/experimental/registry/cache_opts.go b/internal/experimental/registry/cache_opts.go
deleted file mode 100644
index 6851ae807..000000000
--- a/internal/experimental/registry/cache_opts.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"io"
-)
-
-type (
-	// CacheOption allows specifying various settings configurable by the user for overriding the defaults
-	// used when creating a new default cache
-	CacheOption func(*Cache)
-)
-
-// CacheOptDebug returns a function that sets the debug setting on cache options set
-func CacheOptDebug(debug bool) CacheOption {
-	return func(cache *Cache) {
-		cache.debug = debug
-	}
-}
-
-// CacheOptWriter returns a function that sets the writer setting on cache options set
-func CacheOptWriter(out io.Writer) CacheOption {
-	return func(cache *Cache) {
-		cache.out = out
-	}
-}
-
-// CacheOptRoot returns a function that sets the root directory setting on cache options set
-func CacheOptRoot(rootDir string) CacheOption {
-	return func(cache *Cache) {
-		cache.rootDir = rootDir
-	}
-}
diff --git a/internal/experimental/registry/client.go b/internal/experimental/registry/client.go
index 3a499b815..2e8b089aa 100644
--- a/internal/experimental/registry/client.go
+++ b/internal/experimental/registry/client.go
@@ -17,325 +17,458 @@ limitations under the License.
 package registry // import "helm.sh/helm/v3/internal/experimental/registry"
 
 import (
-	"bytes"
-	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"net/http"
-	"sort"
+	"strings"
 
-	"github.com/gosuri/uitable"
+	"github.com/containerd/containerd/remotes"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
-	auth "oras.land/oras-go/pkg/auth/docker"
+	"oras.land/oras-go/pkg/auth"
+	dockerauth "oras.land/oras-go/pkg/auth/docker"
 	"oras.land/oras-go/pkg/content"
 	"oras.land/oras-go/pkg/oras"
 
+	"helm.sh/helm/v3/internal/version"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/helmpath"
 )
 
-const (
-	// CredentialsFileBasename is the filename for auth credentials file
-	CredentialsFileBasename = "config.json"
-)
-
 type (
-	// Client works with OCI-compliant registries and local Helm chart cache
+	// Client works with OCI-compliant registries
 	Client struct {
 		debug bool
 		// path to repository config file e.g. ~/.docker/config.json
 		credentialsFile string
 		out             io.Writer
-		authorizer      *Authorizer
-		resolver        *Resolver
-		cache           *Cache
-		columnWidth     uint
+		authorizer      auth.Client
+		resolver        remotes.Resolver
 	}
+
+	// ClientOption allows specifying various settings configurable by the user for overriding the defaults
+	// used when creating a new default client
+	ClientOption func(*Client)
 )
 
 // NewClient returns a new registry client with config
-func NewClient(opts ...ClientOption) (*Client, error) {
+func NewClient(options ...ClientOption) (*Client, error) {
 	client := &Client{
 		out: ioutil.Discard,
 	}
-	for _, opt := range opts {
-		opt(client)
+	for _, option := range options {
+		option(client)
 	}
-	// set defaults if fields are missing
 	if client.credentialsFile == "" {
 		client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename)
 	}
 	if client.authorizer == nil {
-		authClient, err := auth.NewClient(client.credentialsFile)
+		authClient, err := dockerauth.NewClient(client.credentialsFile)
 		if err != nil {
 			return nil, err
 		}
-		client.authorizer = &Authorizer{
-			Client: authClient,
-		}
+		client.authorizer = authClient
 	}
 	if client.resolver == nil {
-		resolver, err := client.authorizer.Resolver(context.Background(), http.DefaultClient, false)
+		headers := http.Header{}
+		headers.Set("User-Agent", version.GetUserAgent())
+		opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
+		resolver, err := client.authorizer.ResolverWithOpts(opts...)
 		if err != nil {
 			return nil, err
 		}
-		client.resolver = &Resolver{
-			Resolver: resolver,
-		}
+		client.resolver = resolver
 	}
-	if client.cache == nil {
-		cache, err := NewCache(
-			CacheOptDebug(client.debug),
-			CacheOptWriter(client.out),
-			CacheOptRoot(helmpath.CachePath("registry", CacheRootDir)),
-		)
-		if err != nil {
-			return nil, err
-		}
-		client.cache = cache
+	return client, nil
+}
+
+// ClientOptDebug returns a function that sets the debug setting on client options set
+func ClientOptDebug(debug bool) ClientOption {
+	return func(client *Client) {
+		client.debug = debug
 	}
+}
 
-	if client.columnWidth == 0 {
-		client.columnWidth = 60
+// ClientOptWriter returns a function that sets the writer setting on client options set
+func ClientOptWriter(out io.Writer) ClientOption {
+	return func(client *Client) {
+		client.out = out
+	}
+}
+
+// ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set
+func ClientOptCredentialsFile(credentialsFile string) ClientOption {
+	return func(client *Client) {
+		client.credentialsFile = credentialsFile
 	}
-	return client, nil
 }
 
+type (
+	// LoginOption allows specifying various settings on login
+	LoginOption func(*loginOperation)
+
+	loginOperation struct {
+		username string
+		password string
+		insecure bool
+	}
+)
+
 // Login logs into a registry
-func (c *Client) Login(hostname string, username string, password string, insecure bool) error {
-	err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password, insecure)
-	if err != nil {
+func (c *Client) Login(host string, options ...LoginOption) error {
+	operation := &loginOperation{}
+	for _, option := range options {
+		option(operation)
+	}
+	authorizerLoginOpts := []auth.LoginOption{
+		auth.WithLoginContext(ctx(c.out, c.debug)),
+		auth.WithLoginHostname(host),
+		auth.WithLoginUsername(operation.username),
+		auth.WithLoginSecret(operation.password),
+		auth.WithLoginUserAgent(version.GetUserAgent()),
+	}
+	if operation.insecure {
+		authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
+	}
+	if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil {
 		return err
 	}
-	fmt.Fprintf(c.out, "Login succeeded\n")
+	fmt.Fprintln(c.out, "Login Succeeded")
 	return nil
 }
 
-// Logout logs out of a registry
-func (c *Client) Logout(hostname string) error {
-	err := c.authorizer.Logout(ctx(c.out, c.debug), hostname)
-	if err != nil {
-		return err
+// LoginOptBasicAuth returns a function that sets the username/password settings on login
+func LoginOptBasicAuth(username string, password string) LoginOption {
+	return func(operation *loginOperation) {
+		operation.username = username
+		operation.password = password
 	}
-	fmt.Fprintln(c.out, "Logout succeeded")
-	return nil
 }
 
-// PushChart uploads a chart to a registry
-func (c *Client) PushChart(ref *Reference) error {
-	r, err := c.cache.FetchReference(ref)
-	if err != nil {
-		return err
+// LoginOptInsecure returns a function that sets the insecure setting on login
+func LoginOptInsecure(insecure bool) LoginOption {
+	return func(operation *loginOperation) {
+		operation.insecure = insecure
 	}
-	if !r.Exists {
-		return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
+}
+
+type (
+	// LogoutOption allows specifying various settings on logout
+	LogoutOption func(*logoutOperation)
+
+	logoutOperation struct{}
+)
+
+// Logout logs out of a registry
+func (c *Client) Logout(host string, opts ...LogoutOption) error {
+	operation := &logoutOperation{}
+	for _, opt := range opts {
+		opt(operation)
 	}
-	fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo)
-	c.printCacheRefSummary(r)
-	layers := []ocispec.Descriptor{*r.ContentLayer}
-	_, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers,
-		oras.WithConfig(*r.Config), oras.WithNameValidation(nil))
-	if err != nil {
+	if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil {
 		return err
 	}
-	s := ""
-	numLayers := len(layers)
-	if 1 < numLayers {
-		s = "s"
-	}
-	fmt.Fprintf(c.out,
-		"%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size))
+	fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
 	return nil
 }
 
-// PullChart downloads a chart from a registry
-func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) {
-	buf := bytes.NewBuffer(nil)
+type (
+	// PullOption allows specifying various settings on pull
+	PullOption func(*pullOperation)
 
-	if ref.Tag == "" {
-		return buf, errors.New("tag explicitly required")
+	// PullResult is the result returned upon successful pull.
+	PullResult struct {
+		Manifest *descriptorPullSummary         `json:"manifest"`
+		Config   *descriptorPullSummary         `json:"config"`
+		Chart    *descriptorPullSummaryWithMeta `json:"chart"`
+		Prov     *descriptorPullSummary         `json:"prov"`
+		Ref      string                         `json:"ref"`
 	}
 
-	fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
-
-	store := content.NewMemoryStore()
-	fullname := ref.FullName()
-	_ = fullname
-	_, layerDescriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), store,
-		oras.WithPullEmptyNameAllowed(),
-		oras.WithAllowedMediaTypes(KnownMediaTypes()))
-	if err != nil {
-		return buf, err
+	descriptorPullSummary struct {
+		Data   []byte `json:"-"`
+		Digest string `json:"digest"`
+		Size   int64  `json:"size"`
 	}
 
-	numLayers := len(layerDescriptors)
-	if numLayers < 1 {
-		return buf, errors.New(
-			fmt.Sprintf("manifest does not contain at least 1 layer (total: %d)", numLayers))
+	descriptorPullSummaryWithMeta struct {
+		descriptorPullSummary
+		Meta *chart.Metadata `json:"meta"`
 	}
 
-	var contentLayer *ocispec.Descriptor
-	for _, layer := range layerDescriptors {
-		layer := layer
-		switch layer.MediaType {
-		case HelmChartContentLayerMediaType:
-			contentLayer = &layer
-
-		}
+	pullOperation struct {
+		withChart         bool
+		withProv          bool
+		ignoreMissingProv bool
 	}
+)
 
-	if contentLayer == nil {
-		return buf, errors.New(
-			fmt.Sprintf("manifest does not contain a layer with mediatype %s",
-				HelmChartContentLayerMediaType))
+// Pull downloads a chart from a registry
+func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
+	operation := &pullOperation{
+		withChart: true, // By default, always download the chart layer
 	}
-
-	_, b, ok := store.Get(*contentLayer)
-	if !ok {
-		return buf, errors.Errorf("Unable to retrieve blob with digest %s", contentLayer.Digest)
+	for _, option := range options {
+		option(operation)
 	}
-
-	buf = bytes.NewBuffer(b)
-	return buf, nil
-}
-
-// PullChartToCache pulls a chart from an OCI Registry to the Registry Cache.
-// This function is needed for `helm chart pull`, which is experimental and will be deprecated soon.
-// Likewise, the Registry cache will soon be deprecated as will this function.
-func (c *Client) PullChartToCache(ref *Reference) error {
-	if ref.Tag == "" {
-		return errors.New("tag explicitly required")
+	if !operation.withChart && !operation.withProv {
+		return nil, errors.New(
+			"must specify at least one layer to pull (chart/prov)")
 	}
-	existing, err := c.cache.FetchReference(ref)
-	if err != nil {
-		return err
+	store := content.NewMemoryStore()
+	allowedMediaTypes := []string{
+		ConfigMediaType,
+	}
+	minNumDescriptors := 1 // 1 for the config
+	if operation.withChart {
+		minNumDescriptors++
+		allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType)
 	}
-	fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
-	manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(),
+	if operation.withProv {
+		if !operation.ignoreMissingProv {
+			minNumDescriptors++
+		}
+		allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType)
+	}
+	manifest, descriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref, store,
 		oras.WithPullEmptyNameAllowed(),
-		oras.WithAllowedMediaTypes(KnownMediaTypes()),
-		oras.WithContentProvideIngester(c.cache.ProvideIngester()))
+		oras.WithAllowedMediaTypes(allowedMediaTypes))
 	if err != nil {
-		return err
+		return nil, err
 	}
-	err = c.cache.AddManifest(ref, &manifest)
-	if err != nil {
-		return err
+	numDescriptors := len(descriptors)
+	if numDescriptors < minNumDescriptors {
+		return nil, errors.New(
+			fmt.Sprintf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d",
+				minNumDescriptors, numDescriptors))
+	}
+	var configDescriptor *ocispec.Descriptor
+	var chartDescriptor *ocispec.Descriptor
+	var provDescriptor *ocispec.Descriptor
+	for _, descriptor := range descriptors {
+		d := descriptor
+		switch d.MediaType {
+		case ConfigMediaType:
+			configDescriptor = &d
+		case ChartLayerMediaType:
+			chartDescriptor = &d
+		case ProvLayerMediaType:
+			provDescriptor = &d
+		}
 	}
-	r, err := c.cache.FetchReference(ref)
-	if err != nil {
-		return err
+	if configDescriptor == nil {
+		return nil, errors.New(
+			fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType))
 	}
-	if !r.Exists {
-		return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
+	if operation.withChart && chartDescriptor == nil {
+		return nil, errors.New(
+			fmt.Sprintf("manifest does not contain a layer with mediatype %s",
+				ChartLayerMediaType))
+	}
+	var provMissing bool
+	if operation.withProv && provDescriptor == nil {
+		if operation.ignoreMissingProv {
+			provMissing = true
+		} else {
+			return nil, errors.New(
+				fmt.Sprintf("manifest does not contain a layer with mediatype %s",
+					ProvLayerMediaType))
+		}
 	}
-	c.printCacheRefSummary(r)
-	if !existing.Exists {
-		fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName())
+	result := &PullResult{
+		Manifest: &descriptorPullSummary{
+			Digest: manifest.Digest.String(),
+			Size:   manifest.Size,
+		},
+		Config: &descriptorPullSummary{
+			Digest: configDescriptor.Digest.String(),
+			Size:   configDescriptor.Size,
+		},
+		Chart: &descriptorPullSummaryWithMeta{},
+		Prov:  &descriptorPullSummary{},
+		Ref:   ref,
+	}
+	var getManifestErr error
+	if _, manifestData, ok := store.Get(manifest); !ok {
+		getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest)
 	} else {
-		fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName())
+		result.Manifest.Data = manifestData
 	}
-	return err
-}
-
-// SaveChart stores a copy of chart in local cache
-func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error {
-	r, err := c.cache.StoreReference(ref, ch)
-	if err != nil {
-		return err
+	if getManifestErr != nil {
+		return nil, getManifestErr
 	}
-	c.printCacheRefSummary(r)
-	err = c.cache.AddManifest(ref, r.Manifest)
-	if err != nil {
-		return err
+	var getConfigDescriptorErr error
+	if _, configData, ok := store.Get(*configDescriptor); !ok {
+		getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest)
+	} else {
+		result.Config.Data = configData
+		var meta *chart.Metadata
+		if err := json.Unmarshal(configData, &meta); err != nil {
+			return nil, err
+		}
+		result.Chart.Meta = meta
+	}
+	if getConfigDescriptorErr != nil {
+		return nil, getConfigDescriptorErr
+	}
+	if operation.withChart {
+		var getChartDescriptorErr error
+		if _, chartData, ok := store.Get(*chartDescriptor); !ok {
+			getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest)
+		} else {
+			result.Chart.Data = chartData
+			result.Chart.Digest = chartDescriptor.Digest.String()
+			result.Chart.Size = chartDescriptor.Size
+		}
+		if getChartDescriptorErr != nil {
+			return nil, getChartDescriptorErr
+		}
 	}
-	fmt.Fprintf(c.out, "%s: saved\n", r.Tag)
-	return nil
+	if operation.withProv && !provMissing {
+		var getProvDescriptorErr error
+		if _, provData, ok := store.Get(*provDescriptor); !ok {
+			getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest)
+		} else {
+			result.Prov.Data = provData
+			result.Prov.Digest = provDescriptor.Digest.String()
+			result.Prov.Size = provDescriptor.Size
+		}
+		if getProvDescriptorErr != nil {
+			return nil, getProvDescriptorErr
+		}
+	}
+	fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
+	fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
+	return result, nil
 }
 
-// LoadChart retrieves a chart object by reference
-func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) {
-	r, err := c.cache.FetchReference(ref)
-	if err != nil {
-		return nil, err
+// PullOptWithChart returns a function that sets the withChart setting on pull
+func PullOptWithChart(withChart bool) PullOption {
+	return func(operation *pullOperation) {
+		operation.withChart = withChart
 	}
-	if !r.Exists {
-		return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
-	}
-	c.printCacheRefSummary(r)
-	return r.Chart, nil
 }
 
-// RemoveChart deletes a locally saved chart
-func (c *Client) RemoveChart(ref *Reference) error {
-	r, err := c.cache.DeleteReference(ref)
-	if err != nil {
-		return err
+// PullOptWithProv returns a function that sets the withProv setting on pull
+func PullOptWithProv(withProv bool) PullOption {
+	return func(operation *pullOperation) {
+		operation.withProv = withProv
 	}
-	if !r.Exists {
-		return errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
+}
+
+// PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull
+func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption {
+	return func(operation *pullOperation) {
+		operation.ignoreMissingProv = ignoreMissingProv
 	}
-	fmt.Fprintf(c.out, "%s: removed\n", r.Tag)
-	return nil
 }
 
-// PrintChartTable prints a list of locally stored charts
-func (c *Client) PrintChartTable() error {
-	table := uitable.New()
-	table.MaxColWidth = c.columnWidth
-	table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED")
-	rows, err := c.getChartTableRows()
-	if err != nil {
-		return err
+type (
+	// PushOption allows specifying various settings on push
+	PushOption func(*pushOperation)
+
+	// PushResult is the result returned upon successful push.
+	PushResult struct {
+		Manifest *descriptorPushSummary         `json:"manifest"`
+		Config   *descriptorPushSummary         `json:"config"`
+		Chart    *descriptorPushSummaryWithMeta `json:"chart"`
+		Prov     *descriptorPushSummary         `json:"prov"`
+		Ref      string                         `json:"ref"`
 	}
-	for _, row := range rows {
-		table.AddRow(row...)
+
+	descriptorPushSummary struct {
+		Digest string `json:"digest"`
+		Size   int64  `json:"size"`
 	}
-	fmt.Fprintln(c.out, table.String())
-	return nil
-}
 
-// printCacheRefSummary prints out chart ref summary
-func (c *Client) printCacheRefSummary(r *CacheRefSummary) {
-	fmt.Fprintf(c.out, "ref:     %s\n", r.Name)
-	fmt.Fprintf(c.out, "digest:  %s\n", r.Manifest.Digest.Hex())
-	fmt.Fprintf(c.out, "size:    %s\n", byteCountBinary(r.Size))
-	fmt.Fprintf(c.out, "name:    %s\n", r.Chart.Metadata.Name)
-	fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version)
-}
+	descriptorPushSummaryWithMeta struct {
+		descriptorPushSummary
+		Meta *chart.Metadata `json:"meta"`
+	}
 
-// getChartTableRows returns rows in uitable-friendly format
-func (c *Client) getChartTableRows() ([][]interface{}, error) {
-	rr, err := c.cache.ListReferences()
+	pushOperation struct {
+		provData   []byte
+		strictMode bool
+	}
+)
+
+// Push uploads a chart to a registry.
+func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) {
+	operation := &pushOperation{
+		strictMode: true, // By default, enable strict mode
+	}
+	for _, option := range options {
+		option(operation)
+	}
+	meta, err := extractChartMeta(data)
 	if err != nil {
 		return nil, err
 	}
-	refsMap := map[string]map[string]string{}
-	for _, r := range rr {
-		refsMap[r.Name] = map[string]string{
-			"name":    r.Chart.Metadata.Name,
-			"version": r.Chart.Metadata.Version,
-			"digest":  shortDigest(r.Manifest.Digest.Hex()),
-			"size":    byteCountBinary(r.Size),
-			"created": timeAgo(r.CreatedAt),
+	if operation.strictMode {
+		if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) {
+			return nil, errors.New(
+				"strict mode enabled, ref basename and tag must match the chart name and version")
 		}
 	}
-	// Sort and convert to format expected by uitable
-	rows := make([][]interface{}, len(refsMap))
-	keys := make([]string, 0, len(refsMap))
-	for key := range refsMap {
-		keys = append(keys, key)
-	}
-	sort.Strings(keys)
-	for i, key := range keys {
-		rows[i] = make([]interface{}, 6)
-		rows[i][0] = key
-		ref := refsMap[key]
-		for j, k := range []string{"name", "version", "digest", "size", "created"} {
-			rows[i][j+1] = ref[k]
+	store := content.NewMemoryStore()
+	chartDescriptor := store.Add("", ChartLayerMediaType, data)
+	configData, err := json.Marshal(meta)
+	if err != nil {
+		return nil, err
+	}
+	configDescriptor := store.Add("", ConfigMediaType, configData)
+	descriptors := []ocispec.Descriptor{chartDescriptor}
+	var provDescriptor ocispec.Descriptor
+	if operation.provData != nil {
+		provDescriptor = store.Add("", ProvLayerMediaType, operation.provData)
+		descriptors = append(descriptors, provDescriptor)
+	}
+	manifest, err := oras.Push(ctx(c.out, c.debug), c.resolver, ref, store, descriptors,
+		oras.WithConfig(configDescriptor), oras.WithNameValidation(nil))
+	if err != nil {
+		return nil, err
+	}
+	chartSummary := &descriptorPushSummaryWithMeta{
+		Meta: meta,
+	}
+	chartSummary.Digest = chartDescriptor.Digest.String()
+	chartSummary.Size = chartDescriptor.Size
+	result := &PushResult{
+		Manifest: &descriptorPushSummary{
+			Digest: manifest.Digest.String(),
+			Size:   manifest.Size,
+		},
+		Config: &descriptorPushSummary{
+			Digest: configDescriptor.Digest.String(),
+			Size:   configDescriptor.Size,
+		},
+		Chart: chartSummary,
+		Prov:  &descriptorPushSummary{}, // prevent nil references
+		Ref:   ref,
+	}
+	if operation.provData != nil {
+		result.Prov = &descriptorPushSummary{
+			Digest: provDescriptor.Digest.String(),
+			Size:   provDescriptor.Size,
 		}
 	}
-	return rows, nil
+	fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
+	fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
+	return result, err
+}
+
+// PushOptProvData returns a function that sets the prov bytes setting on push
+func PushOptProvData(provData []byte) PushOption {
+	return func(operation *pushOperation) {
+		operation.provData = provData
+	}
+}
+
+// PushOptStrictMode returns a function that sets the strictMode setting on push
+func PushOptStrictMode(strictMode bool) PushOption {
+	return func(operation *pushOperation) {
+		operation.strictMode = strictMode
+	}
 }
diff --git a/internal/experimental/registry/client_opts.go b/internal/experimental/registry/client_opts.go
deleted file mode 100644
index 266cf3f6f..000000000
--- a/internal/experimental/registry/client_opts.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"io"
-)
-
-type (
-	// ClientOption allows specifying various settings configurable by the user for overriding the defaults
-	// used when creating a new default client
-	ClientOption func(*Client)
-)
-
-// ClientOptDebug returns a function that sets the debug setting on client options set
-func ClientOptDebug(debug bool) ClientOption {
-	return func(client *Client) {
-		client.debug = debug
-	}
-}
-
-// ClientOptWriter returns a function that sets the writer setting on client options set
-func ClientOptWriter(out io.Writer) ClientOption {
-	return func(client *Client) {
-		client.out = out
-	}
-}
-
-// ClientOptResolver returns a function that sets the resolver setting on client options set
-func ClientOptResolver(resolver *Resolver) ClientOption {
-	return func(client *Client) {
-		client.resolver = resolver
-	}
-}
-
-// ClientOptAuthorizer returns a function that sets the authorizer setting on client options set
-func ClientOptAuthorizer(authorizer *Authorizer) ClientOption {
-	return func(client *Client) {
-		client.authorizer = authorizer
-	}
-}
-
-// ClientOptCache returns a function that sets the cache setting on a client options set
-func ClientOptCache(cache *Cache) ClientOption {
-	return func(client *Client) {
-		client.cache = cache
-	}
-}
-
-// ClientOptCredentialsFile returns a function that sets the cache setting on a client options set
-func ClientOptCredentialsFile(credentialsFile string) ClientOption {
-	return func(client *Client) {
-		client.credentialsFile = credentialsFile
-	}
-}
-
-// ClientOptColumnWidth returns a function that sets the column width on a client options set
-func ClientOptColumnWidth(columnWidth uint) ClientOption {
-	return func(client *Client) {
-		client.columnWidth = columnWidth
-	}
-}
diff --git a/internal/experimental/registry/constants.go b/internal/experimental/registry/constants.go
index dafb3c9e5..7f870bc20 100644
--- a/internal/experimental/registry/constants.go
+++ b/internal/experimental/registry/constants.go
@@ -17,17 +17,18 @@ limitations under the License.
 package registry // import "helm.sh/helm/v3/internal/experimental/registry"
 
 const (
-	// HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config
-	HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
+	// OCIScheme is the URL scheme for OCI-based requests
+	OCIScheme = "oci"
 
-	// HelmChartContentLayerMediaType is the reserved media type for Helm chart package content
-	HelmChartContentLayerMediaType = "application/tar+gzip"
-)
+	// CredentialsFileBasename is the filename for auth credentials file
+	CredentialsFileBasename = "config.json"
+
+	// ConfigMediaType is the reserved media type for the Helm chart manifest config
+	ConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
 
-// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about
-func KnownMediaTypes() []string {
-	return []string{
-		HelmChartConfigMediaType,
-		HelmChartContentLayerMediaType,
-	}
-}
+	// ChartLayerMediaType is the reserved media type for Helm chart package content
+	ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
+
+	// ProvLayerMediaType is the reserved media type for Helm chart provenance files
+	ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
+)
diff --git a/internal/experimental/registry/reference.go b/internal/experimental/registry/reference.go
deleted file mode 100644
index f0e91d4ba..000000000
--- a/internal/experimental/registry/reference.go
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"errors"
-	"fmt"
-	"net/url"
-	"regexp"
-	"strconv"
-	"strings"
-)
-
-var (
-	validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117
-	// TODO: Currently we don't support digests, so we are only splitting on the
-	// colon. However, when we add support for digests, we'll need to use the
-	// regexp anyway to split on both colons and @, so leaving it like this for
-	// now
-	referenceDelimiter = regexp.MustCompile(`[:]`)
-	errEmptyRepo       = errors.New("parsed repo was empty")
-	errTooManyColons   = errors.New("ref may only contain a single colon character (:) unless specifying a port number")
-)
-
-type (
-	// Reference defines the main components of a reference specification
-	Reference struct {
-		Tag  string
-		Repo string
-	}
-)
-
-// ParseReference converts a string to a Reference
-func ParseReference(s string) (*Reference, error) {
-	if s == "" {
-		return nil, errEmptyRepo
-	}
-	// Split the components of the string on the colon or @, if it is more than 3,
-	// immediately return an error. Other validation will be performed later in
-	// the function
-	splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1))
-	if len(splitComponents) > 3 {
-		return nil, errTooManyColons
-	}
-
-	var ref *Reference
-	switch len(splitComponents) {
-	case 1:
-		ref = &Reference{Repo: splitComponents[0]}
-	case 2:
-		ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]}
-	case 3:
-		ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]}
-	}
-
-	// ensure the reference is valid
-	err := ref.validate()
-	if err != nil {
-		return nil, err
-	}
-
-	return ref, nil
-}
-
-// FullName the full name of a reference (repo:tag)
-func (ref *Reference) FullName() string {
-	if ref.Tag == "" {
-		return ref.Repo
-	}
-	return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
-}
-
-// validate makes sure the ref meets our criteria
-func (ref *Reference) validate() error {
-
-	err := ref.validateRepo()
-	if err != nil {
-		return err
-	}
-	return ref.validateNumColons()
-}
-
-// validateRepo checks that the Repo field is non-empty
-func (ref *Reference) validateRepo() error {
-	if ref.Repo == "" {
-		return errEmptyRepo
-	}
-	// Makes sure the repo results in a parsable URL (similar to what is done
-	// with containerd reference parsing)
-	_, err := url.Parse("//" + ref.Repo)
-	return err
-}
-
-// validateNumColons ensures the ref only contains a single colon character (:)
-// (or potentially two, there might be a port number specified i.e. :5000)
-func (ref *Reference) validateNumColons() error {
-	if strings.Contains(ref.Tag, ":") {
-		return errTooManyColons
-	}
-	parts := strings.Split(ref.Repo, ":")
-	lastIndex := len(parts) - 1
-	if 1 < lastIndex {
-		return errTooManyColons
-	}
-	if 0 < lastIndex {
-		port := strings.Split(parts[lastIndex], "/")[0]
-		if !isValidPort(port) {
-			return errTooManyColons
-		}
-	}
-	return nil
-}
-
-// isValidPort returns whether or not a string looks like a valid port
-func isValidPort(s string) bool {
-	return validPortRegEx.MatchString(s)
-}
-
-// fixSplitComponents this will modify reference parts based on presence of port
-// Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0}
-func fixSplitComponents(c []string) []string {
-	if len(c) <= 1 {
-		return c
-	}
-	possiblePortParts := strings.Split(c[1], "/")
-	if _, err := strconv.Atoi(possiblePortParts[0]); err == nil {
-		components := []string{strings.Join(c[:2], ":")}
-		components = append(components, c[2:]...)
-		return components
-	}
-	return c
-}
diff --git a/internal/experimental/registry/util.go b/internal/experimental/registry/util.go
index 9cb744e3f..257e7af87 100644
--- a/internal/experimental/registry/util.go
+++ b/internal/experimental/registry/util.go
@@ -17,41 +17,31 @@ limitations under the License.
 package registry // import "helm.sh/helm/v3/internal/experimental/registry"
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
-	"time"
+	"strings"
 
-	units "github.com/docker/go-units"
 	"github.com/sirupsen/logrus"
 	orascontext "oras.land/oras-go/pkg/context"
+
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chart/loader"
 )
 
-// byteCountBinary produces a human-readable file size
-func byteCountBinary(b int64) string {
-	const unit = 1024
-	if b < unit {
-		return fmt.Sprintf("%d B", b)
-	}
-	div, exp := int64(unit), 0
-	for n := b / unit; n >= unit; n /= unit {
-		div *= unit
-		exp++
-	}
-	return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
+// IsOCI determines whether or not a URL is to be treated as an OCI URL
+func IsOCI(url string) bool {
+	return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
 }
 
-// shortDigest returns first 7 characters of a sha256 digest
-func shortDigest(digest string) string {
-	if len(digest) == 64 {
-		return digest[:7]
+// extractChartMeta is used to extract a chart metadata from a byte array
+func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
+	ch, err := loader.LoadArchive(bytes.NewReader(chartData))
+	if err != nil {
+		return nil, err
 	}
-	return digest
-}
-
-// timeAgo returns a human-readable timestamp representing time that has passed
-func timeAgo(t time.Time) string {
-	return units.HumanDuration(time.Now().UTC().Sub(t))
+	return ch.Metadata, nil
 }
 
 // ctx retrieves a fresh context.
diff --git a/internal/experimental/uploader/chart_uploader.go b/internal/experimental/uploader/chart_uploader.go
new file mode 100644
index 000000000..87a9d5772
--- /dev/null
+++ b/internal/experimental/uploader/chart_uploader.go
@@ -0,0 +1,58 @@
+/*
+Copyright The Helm Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package uploader
+
+import (
+	"fmt"
+	"io"
+	"net/url"
+
+	"github.com/pkg/errors"
+
+	"helm.sh/helm/v3/internal/experimental/pusher"
+	"helm.sh/helm/v3/internal/experimental/registry"
+)
+
+// ChartUploader handles uploading a chart.
+type ChartUploader struct {
+	// Out is the location to write warning and info messages.
+	Out io.Writer
+	// Pusher collection for the operation
+	Pushers pusher.Providers
+	// Options provide parameters to be passed along to the Pusher being initialized.
+	Options []pusher.Option
+	// RegistryClient is a client for interacting with registries.
+	RegistryClient *registry.Client
+}
+
+// UploadTo uploads a chart. Depending on the settings, it may also upload a provenance file.
+func (c *ChartUploader) UploadTo(ref, remote string) error {
+	u, err := url.Parse(remote)
+	if err != nil {
+		return errors.Errorf("invalid chart URL format: %s", remote)
+	}
+
+	if u.Scheme == "" {
+		return errors.New(fmt.Sprintf("scheme prefix missing from remote (e.g. \"%s://\")", registry.OCIScheme))
+	}
+
+	p, err := c.Pushers.ByScheme(u.Scheme)
+	if err != nil {
+		return err
+	}
+
+	return p.Push(ref, u.String(), c.Options...)
+}
diff --git a/internal/experimental/registry/authorizer.go b/internal/experimental/uploader/doc.go
similarity index 66%
rename from internal/experimental/registry/authorizer.go
rename to internal/experimental/uploader/doc.go
index 58b9abe30..45eacbbf5 100644
--- a/internal/experimental/registry/authorizer.go
+++ b/internal/experimental/uploader/doc.go
@@ -1,11 +1,10 @@
 /*
 Copyright The Helm Authors.
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,15 +13,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package registry // import "helm.sh/helm/v3/internal/experimental/registry"
-
-import (
-	"oras.land/oras-go/pkg/auth"
-)
+/*Package uploader provides a library for uploading charts.
 
-type (
-	// Authorizer handles registry auth operations
-	Authorizer struct {
-		auth.Client
-	}
-)
+This package contains tools for uploading charts to registries.
+*/
+package uploader
diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go
index 9c65d2965..70ce6a55b 100644
--- a/internal/resolver/resolver.go
+++ b/internal/resolver/resolver.go
@@ -26,6 +26,7 @@ import (
 	"github.com/Masterminds/semver/v3"
 	"github.com/pkg/errors"
 
+	"helm.sh/helm/v3/internal/experimental/registry"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/gates"
@@ -121,7 +122,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
 		var version string
 		var ok bool
 		found := true
-		if !strings.HasPrefix(d.Repository, "oci://") {
+		if !registry.IsOCI(d.Repository) {
 			repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
 			if err != nil {
 				return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)
diff --git a/pkg/action/chart_export.go b/pkg/action/chart_export.go
deleted file mode 100644
index 75840d8bc..000000000
--- a/pkg/action/chart_export.go
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"fmt"
-	"io"
-	"path/filepath"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-	"helm.sh/helm/v3/pkg/chartutil"
-)
-
-// ChartExport performs a chart export operation.
-type ChartExport struct {
-	cfg *Configuration
-
-	Destination string
-}
-
-// NewChartExport creates a new ChartExport object with the given configuration.
-func NewChartExport(cfg *Configuration) *ChartExport {
-	return &ChartExport{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart export operation
-func (a *ChartExport) Run(out io.Writer, ref string) error {
-	r, err := registry.ParseReference(ref)
-	if err != nil {
-		return err
-	}
-
-	ch, err := a.cfg.RegistryClient.LoadChart(r)
-	if err != nil {
-		return err
-	}
-
-	// Save the chart to local destination directory
-	err = chartutil.SaveDir(ch, a.Destination)
-	if err != nil {
-		return err
-	}
-
-	d := filepath.Join(a.Destination, ch.Metadata.Name)
-	fmt.Fprintf(out, "Exported chart to %s/\n", d)
-	return nil
-}
diff --git a/pkg/action/chart_list.go b/pkg/action/chart_list.go
deleted file mode 100644
index ba49a887d..000000000
--- a/pkg/action/chart_list.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-)
-
-// ChartList performs a chart list operation.
-type ChartList struct {
-	cfg         *Configuration
-	ColumnWidth uint
-}
-
-// NewChartList creates a new ChartList object with the given configuration.
-func NewChartList(cfg *Configuration) *ChartList {
-	return &ChartList{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart list operation
-func (a *ChartList) Run(out io.Writer) error {
-	client := a.cfg.RegistryClient
-	opt := registry.ClientOptColumnWidth(a.ColumnWidth)
-	opt(client)
-	return client.PrintChartTable()
-}
diff --git a/pkg/action/chart_pull.go b/pkg/action/chart_pull.go
deleted file mode 100644
index 896755201..000000000
--- a/pkg/action/chart_pull.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-)
-
-// ChartPull performs a chart pull operation.
-type ChartPull struct {
-	cfg *Configuration
-}
-
-// NewChartPull creates a new ChartPull object with the given configuration.
-func NewChartPull(cfg *Configuration) *ChartPull {
-	return &ChartPull{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart pull operation
-func (a *ChartPull) Run(out io.Writer, ref string) error {
-	r, err := registry.ParseReference(ref)
-	if err != nil {
-		return err
-	}
-	return a.cfg.RegistryClient.PullChartToCache(r)
-}
diff --git a/pkg/action/chart_push.go b/pkg/action/chart_push.go
deleted file mode 100644
index 91ec49d38..000000000
--- a/pkg/action/chart_push.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-)
-
-// ChartPush performs a chart push operation.
-type ChartPush struct {
-	cfg *Configuration
-}
-
-// NewChartPush creates a new ChartPush object with the given configuration.
-func NewChartPush(cfg *Configuration) *ChartPush {
-	return &ChartPush{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart push operation
-func (a *ChartPush) Run(out io.Writer, ref string) error {
-	r, err := registry.ParseReference(ref)
-	if err != nil {
-		return err
-	}
-	return a.cfg.RegistryClient.PushChart(r)
-}
diff --git a/pkg/action/chart_remove.go b/pkg/action/chart_remove.go
deleted file mode 100644
index 3c0fc2ed7..000000000
--- a/pkg/action/chart_remove.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-)
-
-// ChartRemove performs a chart remove operation.
-type ChartRemove struct {
-	cfg *Configuration
-}
-
-// NewChartRemove creates a new ChartRemove object with the given configuration.
-func NewChartRemove(cfg *Configuration) *ChartRemove {
-	return &ChartRemove{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart remove operation
-func (a *ChartRemove) Run(out io.Writer, ref string) error {
-	r, err := registry.ParseReference(ref)
-	if err != nil {
-		return err
-	}
-	return a.cfg.RegistryClient.RemoveChart(r)
-}
diff --git a/pkg/action/chart_save.go b/pkg/action/chart_save.go
deleted file mode 100644
index 14a2d7c3c..000000000
--- a/pkg/action/chart_save.go
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-	"helm.sh/helm/v3/pkg/chart"
-)
-
-// ChartSave performs a chart save operation.
-type ChartSave struct {
-	cfg *Configuration
-}
-
-// NewChartSave creates a new ChartSave object with the given configuration.
-func NewChartSave(cfg *Configuration) *ChartSave {
-	return &ChartSave{
-		cfg: cfg,
-	}
-}
-
-// Run executes the chart save operation
-func (a *ChartSave) Run(out io.Writer, ch *chart.Chart, ref string) error {
-	r, err := registry.ParseReference(ref)
-	if err != nil {
-		return err
-	}
-
-	// If no tag is present, use the chart version
-	if r.Tag == "" {
-		r.Tag = ch.Metadata.Version
-	}
-
-	return a.cfg.RegistryClient.SaveChart(ch, r)
-}
diff --git a/pkg/action/install.go b/pkg/action/install.go
index 933747d26..10f69d12c 100644
--- a/pkg/action/install.go
+++ b/pkg/action/install.go
@@ -36,6 +36,7 @@ import (
 	"k8s.io/cli-runtime/pkg/resource"
 	"sigs.k8s.io/yaml"
 
+	"helm.sh/helm/v3/internal/experimental/registry"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/cli"
@@ -663,6 +664,14 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
 		RepositoryConfig: settings.RepositoryConfig,
 		RepositoryCache:  settings.RepositoryCache,
 	}
+
+	if registry.IsOCI(name) {
+		if version == "" {
+			return "", errors.New("version is explicitly required for OCI registries")
+		}
+		dl.Options = append(dl.Options, getter.WithTagName(version))
+	}
+
 	if c.Verify {
 		dl.Verify = downloader.VerifyAlways
 	}
@@ -716,5 +725,6 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
 	if version != "" {
 		atVersion = fmt.Sprintf(" at version %q", version)
 	}
-	return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion)
+
+	return filename, errors.Errorf("failed to download %q%s", name, atVersion)
 }
diff --git a/pkg/action/pull.go b/pkg/action/pull.go
index fa1247054..2f5127ea9 100644
--- a/pkg/action/pull.go
+++ b/pkg/action/pull.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/pkg/errors"
 
+	"helm.sh/helm/v3/internal/experimental/registry"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/downloader"
@@ -90,7 +91,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
 		RepositoryCache:  p.Settings.RepositoryCache,
 	}
 
-	if strings.HasPrefix(chartRef, "oci://") {
+	if registry.IsOCI(chartRef) {
 		if p.Version == "" {
 			return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries")
 		}
diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go
index 575c94151..93afb1461 100644
--- a/pkg/downloader/chart_downloader.go
+++ b/pkg/downloader/chart_downloader.go
@@ -102,7 +102,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
 	}
 
 	name := filepath.Base(u.Path)
-	if u.Scheme == "oci" {
+	if u.Scheme == registry.OCIScheme {
 		name = fmt.Sprintf("%s-%s.tgz", name, version)
 	}
 
diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go
index 78add728a..3a0567a87 100644
--- a/pkg/getter/getter.go
+++ b/pkg/getter/getter.go
@@ -169,7 +169,7 @@ var httpProvider = Provider{
 }
 
 var ociProvider = Provider{
-	Schemes: []string{"oci"},
+	Schemes: []string{registry.OCIScheme},
 	New:     NewOCIGetter,
 }
 
diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go
index 3f85b9862..45c92749c 100644
--- a/pkg/getter/ocigetter.go
+++ b/pkg/getter/ocigetter.go
@@ -39,22 +39,30 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
 func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
 	client := g.opts.registryClient
 
-	ref := strings.TrimPrefix(href, "oci://")
+	ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme))
+
+	var pullOpts []registry.PullOption
+	requestingProv := strings.HasSuffix(ref, ".prov")
+	if requestingProv {
+		ref = strings.TrimSuffix(ref, ".prov")
+		pullOpts = append(pullOpts,
+			registry.PullOptWithChart(false),
+			registry.PullOptWithProv(true))
+	}
+
 	if version := g.opts.version; version != "" {
 		ref = fmt.Sprintf("%s:%s", ref, version)
 	}
 
-	r, err := registry.ParseReference(ref)
+	result, err := client.Pull(ref, pullOpts...)
 	if err != nil {
 		return nil, err
 	}
 
-	buf, err := client.PullChart(r)
-	if err != nil {
-		return nil, err
+	if requestingProv {
+		return bytes.NewBuffer(result.Prov.Data), nil
 	}
-
-	return buf, nil
+	return bytes.NewBuffer(result.Chart.Data), nil
 }
 
 // NewOCIGetter constructs a valid http/https client as a Getter


Test patch:
diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go
index 33198a9dd..4ffacbdf2 100644
--- a/cmd/helm/dependency_build_test.go
+++ b/cmd/helm/dependency_build_test.go
@@ -45,7 +45,7 @@ func TestDependencyBuildCmd(t *testing.T) {
 
 	ociChartName := "oci-depending-chart"
 	c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
-	if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil {
+	if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
 		t.Fatal(err)
 	}
 	ociSrv.Run(t, repotest.WithDependingChart(c))
@@ -136,6 +136,9 @@ func TestDependencyBuildCmd(t *testing.T) {
 	}
 
 	// OCI dependencies
+	if err := chartutil.SaveDir(c, dir()); err != nil {
+		t.Fatal(err)
+	}
 	cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
 		dir(ociChartName),
 		dir("repositories.yaml"),
diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go
index 642c9aeca..3f708dd42 100644
--- a/cmd/helm/dependency_update_test.go
+++ b/cmd/helm/dependency_update_test.go
@@ -46,7 +46,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
 
 	ociChartName := "oci-depending-chart"
 	c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
-	if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil {
+	if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
 		t.Fatal(err)
 	}
 	ociSrv.Run(t, repotest.WithDependingChart(c))
@@ -133,6 +133,9 @@ func TestDependencyUpdateCmd(t *testing.T) {
 	}
 
 	// test for OCI charts
+	if err := chartutil.SaveDir(c, dir()); err != nil {
+		t.Fatal(err)
+	}
 	cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
 		dir(ociChartName),
 		dir("repositories.yaml"),
diff --git a/internal/experimental/registry/constants_test.go b/internal/experimental/pusher/ocipusher_test.go
similarity index 60%
rename from internal/experimental/registry/constants_test.go
rename to internal/experimental/pusher/ocipusher_test.go
index 9f078e632..27be15b5d 100644
--- a/internal/experimental/registry/constants_test.go
+++ b/internal/experimental/pusher/ocipusher_test.go
@@ -1,11 +1,10 @@
 /*
 Copyright The Helm Authors.
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,16 +13,24 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package registry
+package pusher
 
 import (
 	"testing"
-
-	"github.com/stretchr/testify/assert"
 )
 
-func TestConstants(t *testing.T) {
-	knownMediaTypes := KnownMediaTypes()
-	assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType)
-	assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType)
+func TestNewOCIPusher(t *testing.T) {
+	testfn := func(ops *options) {
+		if ops.registryClient == nil {
+			t.Fatalf("the OCIPusher's registryClient should not be null")
+		}
+	}
+
+	p, err := NewOCIPusher(testfn)
+	if p == nil {
+		t.Error("NewOCIPusher returned nil")
+	}
+	if err != nil {
+		t.Error(err)
+	}
 }
diff --git a/internal/experimental/pusher/pusher_test.go b/internal/experimental/pusher/pusher_test.go
new file mode 100644
index 000000000..a99b1a5db
--- /dev/null
+++ b/internal/experimental/pusher/pusher_test.go
@@ -0,0 +1,68 @@
+/*
+Copyright The Helm Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pusher
+
+import (
+	"testing"
+
+	"helm.sh/helm/v3/internal/experimental/registry"
+	"helm.sh/helm/v3/pkg/cli"
+)
+
+func TestProvider(t *testing.T) {
+	p := Provider{
+		[]string{"one", "three"},
+		func(_ ...Option) (Pusher, error) { return nil, nil },
+	}
+
+	if !p.Provides("three") {
+		t.Error("Expected provider to provide three")
+	}
+}
+
+func TestProviders(t *testing.T) {
+	ps := Providers{
+		{[]string{"one", "three"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
+		{[]string{"two", "four"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
+	}
+
+	if _, err := ps.ByScheme("one"); err != nil {
+		t.Error(err)
+	}
+	if _, err := ps.ByScheme("four"); err != nil {
+		t.Error(err)
+	}
+
+	if _, err := ps.ByScheme("five"); err == nil {
+		t.Error("Did not expect handler for five")
+	}
+}
+
+func TestAll(t *testing.T) {
+	env := cli.New()
+	all := All(env)
+	if len(all) != 1 {
+		t.Errorf("expected 1 provider (OCI), got %d", len(all))
+	}
+}
+
+func TestByScheme(t *testing.T) {
+	env := cli.New()
+	g := All(env)
+	if _, err := g.ByScheme(registry.OCIScheme); err != nil {
+		t.Error(err)
+	}
+}
diff --git a/internal/experimental/registry/client_test.go b/internal/experimental/registry/client_test.go
index 0d5bb60b2..3666a844f 100644
--- a/internal/experimental/registry/client_test.go
+++ b/internal/experimental/registry/client_test.go
@@ -39,13 +39,10 @@ import (
 	"github.com/phayes/freeport"
 	"github.com/stretchr/testify/suite"
 	"golang.org/x/crypto/bcrypt"
-	auth "oras.land/oras-go/pkg/auth/docker"
-
-	"helm.sh/helm/v3/pkg/chart"
 )
 
 var (
-	testCacheRootDir         = "helm-registry-test"
+	testWorkspaceDir         = "helm-registry-test"
 	testHtpasswdFileBasename = "authtest.htpasswd"
 	testUsername             = "myuser"
 	testPassword             = "mypass"
@@ -56,51 +53,32 @@ type RegistryClientTestSuite struct {
 	Out                     io.Writer
 	DockerRegistryHost      string
 	CompromisedRegistryHost string
-	CacheRootDir            string
+	WorkspaceDir            string
 	RegistryClient          *Client
 }
 
 func (suite *RegistryClientTestSuite) SetupSuite() {
-	suite.CacheRootDir = testCacheRootDir
-	os.RemoveAll(suite.CacheRootDir)
-	os.Mkdir(suite.CacheRootDir, 0700)
+	suite.WorkspaceDir = testWorkspaceDir
+	os.RemoveAll(suite.WorkspaceDir)
+	os.Mkdir(suite.WorkspaceDir, 0700)
 
 	var out bytes.Buffer
 	suite.Out = &out
-	credentialsFile := filepath.Join(suite.CacheRootDir, CredentialsFileBasename)
-
-	client, err := auth.NewClient(credentialsFile)
-	suite.Nil(err, "no error creating auth client")
-
-	resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
-	suite.Nil(err, "no error creating resolver")
-
-	// create cache
-	cache, err := NewCache(
-		CacheOptDebug(true),
-		CacheOptWriter(suite.Out),
-		CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)),
-	)
-	suite.Nil(err, "no error creating cache")
+	credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
 
 	// init test client
+	var err error
 	suite.RegistryClient, err = NewClient(
 		ClientOptDebug(true),
 		ClientOptWriter(suite.Out),
-		ClientOptAuthorizer(&Authorizer{
-			Client: client,
-		}),
-		ClientOptResolver(&Resolver{
-			Resolver: resolver,
-		}),
-		ClientOptCache(cache),
+		ClientOptCredentialsFile(credentialsFile),
 	)
 	suite.Nil(err, "no error creating registry client")
 
 	// create htpasswd file (w BCrypt, which is required)
 	pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
 	suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
-	htpasswdPath := filepath.Join(suite.CacheRootDir, testHtpasswdFileBasename)
+	htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
 	err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
 	suite.Nil(err, "no error creating test htpasswd file")
 
@@ -109,7 +87,7 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
 	port, err := freeport.GetFreePort()
 	suite.Nil(err, "no error finding free port for test registry")
 	suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
-	config.HTTP.Addr = fmt.Sprintf(":%d", port)
+	config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
 	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
 	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
 	config.Auth = configuration.Auth{
@@ -128,110 +106,195 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
 }
 
 func (suite *RegistryClientTestSuite) TearDownSuite() {
-	os.RemoveAll(suite.CacheRootDir)
+	os.RemoveAll(suite.WorkspaceDir)
 }
 
 func (suite *RegistryClientTestSuite) Test_0_Login() {
-	err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", false)
+	err := suite.RegistryClient.Login(suite.DockerRegistryHost,
+		LoginOptBasicAuth("badverybad", "ohsobad"),
+		LoginOptInsecure(false))
 	suite.NotNil(err, "error logging into registry with bad credentials")
 
-	err = suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", true)
+	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
+		LoginOptBasicAuth("badverybad", "ohsobad"),
+		LoginOptInsecure(true))
 	suite.NotNil(err, "error logging into registry with bad credentials, insecure mode")
 
-	err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, false)
+	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
+		LoginOptBasicAuth(testUsername, testPassword),
+		LoginOptInsecure(false))
 	suite.Nil(err, "no error logging into registry with good credentials")
 
-	err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, true)
+	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
+		LoginOptBasicAuth(testUsername, testPassword),
+		LoginOptInsecure(true))
 	suite.Nil(err, "no error logging into registry with good credentials, insecure mode")
 }
 
-func (suite *RegistryClientTestSuite) Test_1_SaveChart() {
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
-	suite.Nil(err)
-
-	// empty chart
-	err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref)
-	suite.NotNil(err)
-
-	// valid chart
-	ch := &chart.Chart{}
-	ch.Metadata = &chart.Metadata{
-		APIVersion: "v1",
-		Name:       "testchart",
-		Version:    "1.2.3",
-	}
-	err = suite.RegistryClient.SaveChart(ch, ref)
-	suite.Nil(err)
+func (suite *RegistryClientTestSuite) Test_1_Push() {
+	// Bad bytes
+	ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
+	_, err := suite.RegistryClient.Push([]byte("hello"), ref)
+	suite.NotNil(err, "error pushing non-chart bytes")
+
+	// Load a test chart
+	chartData, err := ioutil.ReadFile("../../../pkg/repo/repotest/testdata/examplechart-0.1.0.tgz")
+	suite.Nil(err, "no error loading test chart")
+	meta, err := extractChartMeta(chartData)
+	suite.Nil(err, "no error extracting chart meta")
+
+	// non-strict ref (chart name)
+	ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
+	_, err = suite.RegistryClient.Push(chartData, ref)
+	suite.NotNil(err, "error pushing non-strict ref (bad basename)")
+
+	// non-strict ref (chart name), with strict mode disabled
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
+	suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
+
+	// non-strict ref (chart version)
+	ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
+	_, err = suite.RegistryClient.Push(chartData, ref)
+	suite.NotNil(err, "error pushing non-strict ref (bad tag)")
+
+	// non-strict ref (chart version), with strict mode disabled
+	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
+	suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
+
+	// basic push, good ref
+	chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
+	suite.Nil(err, "no error loading test chart")
+	meta, err = extractChartMeta(chartData)
+	suite.Nil(err, "no error extracting chart meta")
+	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
+	_, err = suite.RegistryClient.Push(chartData, ref)
+	suite.Nil(err, "no error pushing good ref")
+
+	_, err = suite.RegistryClient.Pull(ref)
+	suite.Nil(err, "no error pulling a simple chart")
+
+	// Load another test chart
+	chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
+	suite.Nil(err, "no error loading test chart")
+	meta, err = extractChartMeta(chartData)
+	suite.Nil(err, "no error extracting chart meta")
+
+	// Load prov file
+	provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
+	suite.Nil(err, "no error loading test prov")
+
+	// push with prov
+	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
+	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
+	suite.Nil(err, "no error pushing good ref with prov")
+
+	_, err = suite.RegistryClient.Pull(ref)
+	suite.Nil(err, "no error pulling a simple chart")
+
+	// Validate the output
+	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
+	// or if the format of the OCI manifest changes
+	suite.Equal(ref, result.Ref)
+	suite.Equal(meta.Name, result.Chart.Meta.Name)
+	suite.Equal(meta.Version, result.Chart.Meta.Version)
+	suite.Equal(int64(512), result.Manifest.Size)
+	suite.Equal(int64(99), result.Config.Size)
+	suite.Equal(int64(973), result.Chart.Size)
+	suite.Equal(int64(695), result.Prov.Size)
+	suite.Equal(
+		"sha256:c4fd4ca31f12f50a7f704bb1dfdf2e768b1e8bdeac3991b534b6bdb3f535aab1",
+		result.Manifest.Digest)
+	suite.Equal(
+		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
+		result.Config.Digest)
+	suite.Equal(
+		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
+		result.Chart.Digest)
+	suite.Equal(
+		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
+		result.Prov.Digest)
 }
 
-func (suite *RegistryClientTestSuite) Test_2_LoadChart() {
-
-	// non-existent ref
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
-	suite.Nil(err)
-	_, err = suite.RegistryClient.LoadChart(ref)
-	suite.NotNil(err)
-
-	// existing ref
-	ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
-	suite.Nil(err)
-	ch, err := suite.RegistryClient.LoadChart(ref)
-	suite.Nil(err)
-	suite.Equal("testchart", ch.Metadata.Name)
-	suite.Equal("1.2.3", ch.Metadata.Version)
-}
-
-func (suite *RegistryClientTestSuite) Test_3_PushChart() {
-
-	// non-existent ref
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
-	suite.Nil(err)
-	err = suite.RegistryClient.PushChart(ref)
-	suite.NotNil(err)
-
-	// existing ref
-	ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
-	suite.Nil(err)
-	err = suite.RegistryClient.PushChart(ref)
-	suite.Nil(err)
-}
-
-func (suite *RegistryClientTestSuite) Test_4_PullChart() {
-
-	// non-existent ref
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
-	suite.Nil(err)
-	_, err = suite.RegistryClient.PullChart(ref)
-	suite.NotNil(err)
-
-	// existing ref
-	ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
-	suite.Nil(err)
-	_, err = suite.RegistryClient.PullChart(ref)
-	suite.Nil(err)
-}
-
-func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() {
-	err := suite.RegistryClient.PrintChartTable()
-	suite.Nil(err)
-}
-
-func (suite *RegistryClientTestSuite) Test_6_RemoveChart() {
-
-	// non-existent ref
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
-	suite.Nil(err)
-	err = suite.RegistryClient.RemoveChart(ref)
-	suite.NotNil(err)
-
-	// existing ref
-	ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
-	suite.Nil(err)
-	err = suite.RegistryClient.RemoveChart(ref)
-	suite.Nil(err)
+func (suite *RegistryClientTestSuite) Test_2_Pull() {
+	// bad/missing ref
+	ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
+	_, err := suite.RegistryClient.Pull(ref)
+	suite.NotNil(err, "error on bad/missing ref")
+
+	// Load test chart (to build ref pushed in previous test)
+	chartData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
+	suite.Nil(err, "no error loading test chart")
+	meta, err := extractChartMeta(chartData)
+	suite.Nil(err, "no error extracting chart meta")
+	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
+
+	// Simple pull, chart only
+	_, err = suite.RegistryClient.Pull(ref)
+	suite.Nil(err, "no error pulling a simple chart")
+
+	// Simple pull with prov (no prov uploaded)
+	_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
+	suite.NotNil(err, "error pulling a chart with prov when no prov exists")
+
+	// Simple pull with prov, ignoring missing prov
+	_, err = suite.RegistryClient.Pull(ref,
+		PullOptWithProv(true),
+		PullOptIgnoreMissingProv(true))
+	suite.Nil(err,
+		"no error pulling a chart with prov when no prov exists, ignoring missing")
+
+	// Load test chart (to build ref pushed in previous test)
+	chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
+	suite.Nil(err, "no error loading test chart")
+	meta, err = extractChartMeta(chartData)
+	suite.Nil(err, "no error extracting chart meta")
+	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
+
+	// Load prov file
+	provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
+	suite.Nil(err, "no error loading test prov")
+
+	// no chart and no prov causes error
+	_, err = suite.RegistryClient.Pull(ref,
+		PullOptWithChart(false),
+		PullOptWithProv(false))
+	suite.NotNil(err, "error on both no chart and no prov")
+
+	// full pull with chart and prov
+	result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
+	suite.Nil(err, "no error pulling a chart with prov")
+
+	// Validate the output
+	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
+	// or if the format of the OCI manifest changes
+	suite.Equal(ref, result.Ref)
+	suite.Equal(meta.Name, result.Chart.Meta.Name)
+	suite.Equal(meta.Version, result.Chart.Meta.Version)
+	suite.Equal(int64(512), result.Manifest.Size)
+	suite.Equal(int64(99), result.Config.Size)
+	suite.Equal(int64(973), result.Chart.Size)
+	suite.Equal(int64(695), result.Prov.Size)
+	suite.Equal(
+		"sha256:c4fd4ca31f12f50a7f704bb1dfdf2e768b1e8bdeac3991b534b6bdb3f535aab1",
+		result.Manifest.Digest)
+	suite.Equal(
+		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
+		result.Config.Digest)
+	suite.Equal(
+		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
+		result.Chart.Digest)
+	suite.Equal(
+		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
+		result.Prov.Digest)
+	suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973},{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695}]}",
+		string(result.Manifest.Data))
+	suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
+		string(result.Config.Data))
+	suite.Equal(chartData, result.Chart.Data)
+	suite.Equal(provData, result.Prov.Data)
 }
 
-func (suite *RegistryClientTestSuite) Test_7_Logout() {
+func (suite *RegistryClientTestSuite) Test_3_Logout() {
 	err := suite.RegistryClient.Logout("this-host-aint-real:5000")
 	suite.NotNil(err, "error logging out of registry that has no entry")
 
@@ -239,12 +302,11 @@ func (suite *RegistryClientTestSuite) Test_7_Logout() {
 	suite.Nil(err, "no error logging out of registry")
 }
 
-func (suite *RegistryClientTestSuite) Test_8_ManInTheMiddle() {
-	ref, err := ParseReference(fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost))
-	suite.Nil(err)
+func (suite *RegistryClientTestSuite) Test_4_ManInTheMiddle() {
+	ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)
 
 	// returns content that does not match the expected digest
-	_, err = suite.RegistryClient.PullChart(ref)
+	_, err := suite.RegistryClient.Pull(ref)
 	suite.NotNil(err)
 	suite.True(errdefs.IsFailedPrecondition(err))
 }
@@ -261,19 +323,19 @@ func initCompromisedRegistryTestServer() string {
 
 			// layers[0] is the blob []byte("a")
 			w.Write([]byte(
-				`{ "schemaVersion": 2, "config": {
-    "mediaType": "application/vnd.cncf.helm.config.v1+json",
+				fmt.Sprintf(`{ "schemaVersion": 2, "config": {
+    "mediaType": "%s",
     "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
     "size": 181
   },
   "layers": [
     {
-      "mediaType": "application/tar+gzip",
+      "mediaType": "%s",
       "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
       "size": 1
     }
   ]
-}`))
+}`, ConfigMediaType, ChartLayerMediaType)))
 		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
 			w.Header().Set("Content-Type", "application/json")
 			w.WriteHeader(200)
@@ -281,7 +343,7 @@ func initCompromisedRegistryTestServer() string {
 				"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
 				"\"application\"}"))
 		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
-			w.Header().Set("Content-Type", "application/tar+gzip")
+			w.Header().Set("Content-Type", ChartLayerMediaType)
 			w.WriteHeader(200)
 			w.Write([]byte("b"))
 		} else {
diff --git a/internal/experimental/registry/reference_test.go b/internal/experimental/registry/reference_test.go
deleted file mode 100644
index aae03ad99..000000000
--- a/internal/experimental/registry/reference_test.go
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package registry
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestParseReference(t *testing.T) {
-	is := assert.New(t)
-
-	// bad refs
-	s := ""
-	_, err := ParseReference(s)
-	is.Error(err, "empty ref")
-
-	s = "my:bad:ref"
-	_, err = ParseReference(s)
-	is.Error(err, "ref contains too many colons (2)")
-
-	s = "my:really:bad:ref"
-	_, err = ParseReference(s)
-	is.Error(err, "ref contains too many colons (3)")
-
-	// good refs
-	s = "mychart"
-	ref, err := ParseReference(s)
-	is.NoError(err)
-	is.Equal("mychart", ref.Repo)
-	is.Equal("", ref.Tag)
-	is.Equal("mychart", ref.FullName())
-
-	s = "mychart:1.5.0"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("mychart", ref.Repo)
-	is.Equal("1.5.0", ref.Tag)
-	is.Equal("mychart:1.5.0", ref.FullName())
-
-	s = "myrepo/mychart"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("myrepo/mychart", ref.Repo)
-	is.Equal("", ref.Tag)
-	is.Equal("myrepo/mychart", ref.FullName())
-
-	s = "myrepo/mychart:1.5.0"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("myrepo/mychart", ref.Repo)
-	is.Equal("1.5.0", ref.Tag)
-	is.Equal("myrepo/mychart:1.5.0", ref.FullName())
-
-	s = "mychart:5001:1.5.0"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("mychart:5001", ref.Repo)
-	is.Equal("1.5.0", ref.Tag)
-	is.Equal("mychart:5001:1.5.0", ref.FullName())
-
-	s = "myrepo:5001/mychart:1.5.0"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("myrepo:5001/mychart", ref.Repo)
-	is.Equal("1.5.0", ref.Tag)
-	is.Equal("myrepo:5001/mychart:1.5.0", ref.FullName())
-
-	s = "127.0.0.1:5001/mychart:1.5.0"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("127.0.0.1:5001/mychart", ref.Repo)
-	is.Equal("1.5.0", ref.Tag)
-	is.Equal("127.0.0.1:5001/mychart:1.5.0", ref.FullName())
-
-	s = "localhost:5000/mychart:latest"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("localhost:5000/mychart", ref.Repo)
-	is.Equal("latest", ref.Tag)
-	is.Equal("localhost:5000/mychart:latest", ref.FullName())
-
-	s = "my.host.com/my/nested/repo:1.2.3"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("my.host.com/my/nested/repo", ref.Repo)
-	is.Equal("1.2.3", ref.Tag)
-	is.Equal("my.host.com/my/nested/repo:1.2.3", ref.FullName())
-
-	s = "localhost:5000/x/y/z"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("localhost:5000/x/y/z", ref.Repo)
-	is.Equal("", ref.Tag)
-	is.Equal("localhost:5000/x/y/z", ref.FullName())
-
-	s = "localhost:5000/x/y/z:123"
-	ref, err = ParseReference(s)
-	is.NoError(err)
-	is.Equal("localhost:5000/x/y/z", ref.Repo)
-	is.Equal("123", ref.Tag)
-	is.Equal("localhost:5000/x/y/z:123", ref.FullName())
-
-	s = "localhost:5000/x/y/z:123:x"
-	_, err = ParseReference(s)
-	is.Error(err, "ref contains too many colons (3)")
-
-	s = "localhost:5000/x/y/z:123:x:y"
-	_, err = ParseReference(s)
-	is.Error(err, "ref contains too many colons (4)")
-}
diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go
index 282945c17..f8bdff3b7 100644
--- a/pkg/action/action_test.go
+++ b/pkg/action/action_test.go
@@ -16,16 +16,12 @@ limitations under the License.
 package action
 
 import (
-	"context"
 	"flag"
 	"io/ioutil"
-	"net/http"
 	"os"
-	"path/filepath"
 	"testing"
 
 	fakeclientset "k8s.io/client-go/kubernetes/fake"
-	dockerauth "oras.land/oras-go/pkg/auth/docker"
 
 	"helm.sh/helm/v3/internal/experimental/registry"
 	"helm.sh/helm/v3/pkg/chart"
@@ -42,16 +38,6 @@ var verbose = flag.Bool("test.log", false, "enable test logging")
 func actionConfigFixture(t *testing.T) *Configuration {
 	t.Helper()
 
-	client, err := dockerauth.NewClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
 	tdir, err := ioutil.TempDir("", "helm-action-test")
 	if err != nil {
 		t.Fatal(err)
@@ -59,23 +45,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
 
 	t.Cleanup(func() { os.RemoveAll(tdir) })
 
-	cache, err := registry.NewCache(
-		registry.CacheOptDebug(true),
-		registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)),
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	registryClient, err := registry.NewClient(
-		registry.ClientOptAuthorizer(&registry.Authorizer{
-			Client: client,
-		}),
-		registry.ClientOptResolver(&registry.Resolver{
-			Resolver: resolver,
-		}),
-		registry.ClientOptCache(cache),
-	)
+	registryClient, err := registry.NewClient()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/action/chart_save_test.go b/pkg/action/chart_save_test.go
deleted file mode 100644
index 4fd991a4e..000000000
--- a/pkg/action/chart_save_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package action
-
-import (
-	"io/ioutil"
-	"testing"
-
-	"helm.sh/helm/v3/internal/experimental/registry"
-)
-
-func chartSaveAction(t *testing.T) *ChartSave {
-	t.Helper()
-	config := actionConfigFixture(t)
-	action := NewChartSave(config)
-	return action
-}
-
-func TestChartSave(t *testing.T) {
-	action := chartSaveAction(t)
-
-	input := buildChart()
-	if err := action.Run(ioutil.Discard, input, "localhost:5000/test:0.2.0"); err != nil {
-		t.Error(err)
-	}
-
-	ref, err := registry.ParseReference("localhost:5000/test:0.2.0")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
-		t.Error(err)
-	}
-
-	// now let's check if `helm chart save` can use the chart version when the tag is not present
-	if err := action.Run(ioutil.Discard, input, "localhost:5000/test"); err != nil {
-		t.Error(err)
-	}
-
-	ref, err = registry.ParseReference("localhost:5000/test")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// TODO: guess latest based on semver?
-	_, err = action.cfg.RegistryClient.LoadChart(ref)
-	if err == nil {
-		t.Error("Expected error parsing ref without tag")
-	}
-
-	ref.Tag = "0.1.0"
-	if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
-		t.Error(err)
-	}
-}
diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go
index 462c2c299..0a5c3ae69 100644
--- a/pkg/repo/repotest/server.go
+++ b/pkg/repo/repotest/server.go
@@ -32,7 +32,6 @@ import (
 	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry
 	"github.com/phayes/freeport"
 	"golang.org/x/crypto/bcrypt"
-	auth "oras.land/oras-go/pkg/auth/docker"
 	"sigs.k8s.io/yaml"
 
 	ociRegistry "helm.sh/helm/v3/internal/experimental/registry"
@@ -151,40 +150,25 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
 
 	credentialsFile := filepath.Join(srv.Dir, "config.json")
 
-	client, err := auth.NewClient(credentialsFile)
-	if err != nil {
-		t.Fatalf("error creating auth client")
-	}
-
-	resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
-	if err != nil {
-		t.Fatalf("error creating resolver")
-	}
-
 	// init test client
 	registryClient, err := ociRegistry.NewClient(
 		ociRegistry.ClientOptDebug(true),
 		ociRegistry.ClientOptWriter(os.Stdout),
-		ociRegistry.ClientOptAuthorizer(&ociRegistry.Authorizer{
-			Client: client,
-		}),
-		ociRegistry.ClientOptResolver(&ociRegistry.Resolver{
-			Resolver: resolver,
-		}),
+		ociRegistry.ClientOptCredentialsFile(credentialsFile),
 	)
 	if err != nil {
 		t.Fatalf("error creating registry client")
 	}
 
-	err = registryClient.Login(srv.RegistryURL, srv.TestUsername, srv.TestPassword, false)
+	err = registryClient.Login(
+		srv.RegistryURL,
+		ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword),
+		ociRegistry.LoginOptInsecure(false))
 	if err != nil {
 		t.Fatalf("error logging into registry with good credentials")
 	}
 
-	ref, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL))
-	if err != nil {
-		t.Fatalf("")
-	}
+	ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL)
 
 	err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz"))
 	if err != nil {
@@ -202,35 +186,56 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
 		t.Fatal("error removing chart before push")
 	}
 
-	err = registryClient.SaveChart(ch, ref)
+	// save it back to disk..
+	absPath, err := chartutil.Save(ch, srv.Dir)
 	if err != nil {
-		t.Fatal("error saving chart")
+		t.Fatal("could not create chart archive")
 	}
 
-	err = registryClient.PushChart(ref)
+	// load it into memory...
+	contentBytes, err := ioutil.ReadFile(absPath)
 	if err != nil {
-		t.Fatal("error pushing chart")
+		t.Fatal("could not load chart into memory")
 	}
 
-	if cfg.DependingChart != nil {
-		c := cfg.DependingChart
-		dependingRef, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-depending-chart:1.2.3", srv.RegistryURL))
-		if err != nil {
-			t.Fatal("error parsing reference for depending chart reference")
-		}
+	result, err := registryClient.Push(contentBytes, ref)
+	if err != nil {
+		t.Fatalf("error pushing dependent chart: %s", err)
+	}
+	t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
+		"Config.Digest: %s, Config.Size: %d, "+
+		"Chart.Digest: %s, Chart.Size: %d",
+		result.Manifest.Digest, result.Manifest.Size,
+		result.Config.Digest, result.Config.Size,
+		result.Chart.Digest, result.Chart.Size)
 
-		err = registryClient.SaveChart(c, dependingRef)
-		if err != nil {
-			t.Fatal("error saving depending chart")
-		}
+	srv.Client = registryClient
+	c := cfg.DependingChart
+	if c == nil {
+		return
+	}
 
-		err = registryClient.PushChart(dependingRef)
-		if err != nil {
-			t.Fatal("error pushing depending chart")
-		}
+	dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s",
+		srv.RegistryURL, c.Metadata.Name, c.Metadata.Version)
+
+	// load it into memory...
+	absPath = filepath.Join(srv.Dir,
+		fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version))
+	contentBytes, err = ioutil.ReadFile(absPath)
+	if err != nil {
+		t.Fatal("could not load chart into memory")
 	}
 
-	srv.Client = registryClient
+	result, err = registryClient.Push(contentBytes, dependingRef)
+	if err != nil {
+		t.Fatalf("error pushing depending chart: %s", err)
+	}
+	t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
+		"Config.Digest: %s, Config.Size: %d, "+
+		"Chart.Digest: %s, Chart.Size: %d",
+		result.Manifest.Digest, result.Manifest.Size,
+		result.Config.Digest, result.Config.Size,
+		result.Chart.Digest, result.Chart.Size)
 }
 
 // NewTempServer creates a server inside of a temp dir.