Repository: helm/helm (go)

Issue:
helm push using zot is 403
<!-- If you need help or think you have found a bug, please help us with your issue by entering the following information (otherwise you can delete this text): -->

Output of `helm version`:
version.BuildInfo{Version:"v3.12.0", GitCommit:"c9f554d75773799f72ceef38c51210f1842a1dea", GitTreeState:"clean", GoVersion:"go1.20.3"}
Output of `kubectl version`:

Cloud Provider/Platform (AKS, GKE, Minikube etc.): 

1. login 
```
helm registry login 192.168.64.1:6000 --username admin --insecure --password admin --debug
WARNING: Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
```
2. push 
```
helm  push openebs-3.4.1.tgz   oci://192.168.64.1:6000/charts --insecure-skip-tls-verify
Error: unexpected status from POST request to https://192.168.64.1:6000/v2/charts/openebs/blobs/uploads/: 403 Forbidden
```

so I  modify code 

```
if client.resolver == nil {
		headers := http.Header{}
		headers.Set("User-Agent", version.GetUserAgent())
		headers.Set("Authorization", "Basic YWRtaW46YWRtaW4=")
		opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}

		if client.httpClient != nil {
			opts = append(opts, auth.WithResolverClient(client.httpClient))
		}
		resolver, err := client.authorizer.ResolverWithOpts(opts...)
		if err != nil {
			return nil, err
		}
		client.resolver = resolver
	}
```

Upload was successful. I think it's an oci upload error. We need to upgrade the code to support it. If you agree with me, you can assign this issue to me. 


Pull request:
fix(main): fix basic auth for helm pull or push
<!--  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.-->

closes #12234 

**What this PR does / why we need it**:

add pull and push using basic auth or bearer auth

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [ ] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index c25beb83e..d61b3d75e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
+      uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # pinv2.20.3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
+      uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # pinv2.20.3
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
+      uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # pinv2.20.3
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index bc095de77..6ffc968ce 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -142,12 +142,6 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			}
 			client.SetRegistryClient(registryClient)
 
-			// This is for the case where "" is specifically passed in as a
-			// value. When there is no value passed in NoOptDefVal will be used
-			// and it is set to client. See addInstallFlags.
-			if client.DryRunOption == "" {
-				client.DryRunOption = "none"
-			}
 			rel, err := runInstall(args, client, valueOpts, out)
 			if err != nil {
 				return errors.Wrap(err, "INSTALLATION FAILED")
@@ -166,13 +160,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 
 func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
 	f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
-	// --dry-run options with expected outcome:
-	// - Not set means no dry run and server is contacted.
-	// - Set with no value, a value of client, or a value of true and the server is not contacted
-	// - Set with a value of false, none, or false and the server is contacted
-	// The true/false part is meant to reflect some legacy behavior while none is equal to "".
-	f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
-	f.Lookup("dry-run").NoOptDefVal = "client"
+	f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
 	f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
 	f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
 	f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
@@ -281,11 +269,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
 
 	client.Namespace = settings.Namespace()
 
-	// Validate DryRunOption member is one of the allowed values
-	if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
-		return nil, err
-	}
-
 	// Create context and prepare the handle of SIGTERM
 	ctx := context.Background()
 	ctx, cancel := context.WithCancel(ctx)
@@ -326,19 +309,3 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
 	}
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
-
-func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
-	// Validate dry-run flag value with a set of allowed value
-	allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
-	isAllowed := false
-	for _, v := range allowedDryRunValues {
-		if dryRunOptionFlagValue == v {
-			isAllowed = true
-			break
-		}
-	}
-	if !isAllowed {
-		return errors.New("Invalid dry-run flag. Flag must one of the following: false, true, none, client, server")
-	}
-	return nil
-}
diff --git a/cmd/helm/template.go b/cmd/helm/template.go
index f8cd8268b..3bc70f995 100644
--- a/cmd/helm/template.go
+++ b/cmd/helm/template.go
@@ -79,12 +79,6 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			}
 			client.SetRegistryClient(registryClient)
 
-			// This is for the case where "" is specifically passed in as a
-			// value. When there is no value passed in NoOptDefVal will be used
-			// and it is set to client. See addInstallFlags.
-			if client.DryRunOption == "" {
-				client.DryRunOption = "true"
-			}
 			client.DryRun = true
 			client.ReleaseName = "release-name"
 			client.Replace = true // Skip the name check
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 7e6082782..145d342b7 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -96,12 +96,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			}
 			client.SetRegistryClient(registryClient)
 
-			// This is for the case where "" is specifically passed in as a
-			// value. When there is no value passed in NoOptDefVal will be used
-			// and it is set to client. See addInstallFlags.
-			if client.DryRunOption == "" {
-				client.DryRunOption = "none"
-			}
 			// Fixes #7002 - Support reading values from STDIN for `upgrade` command
 			// Must load values AFTER determining if we have to call install so that values loaded from stdin are are not read twice
 			if client.Install {
@@ -118,7 +112,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 					instClient.ChartPathOptions = client.ChartPathOptions
 					instClient.Force = client.Force
 					instClient.DryRun = client.DryRun
-					instClient.DryRunOption = client.DryRunOption
 					instClient.DisableHooks = client.DisableHooks
 					instClient.SkipCRDs = client.SkipCRDs
 					instClient.Timeout = client.Timeout
@@ -153,10 +146,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if err != nil {
 				return err
 			}
-			// Validate dry-run flag value is one of the allowed values
-			if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
-				return err
-			}
 
 			p := getter.All(settings)
 			vals, err := valueOpts.MergeValues(p)
@@ -232,8 +221,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present")
 	f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
 	f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
-	f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
-	f.Lookup("dry-run").NoOptDefVal = "client"
+	f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade")
 	f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
 	f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")
 	f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
diff --git a/go.mod b/go.mod
index 07200b4b9..5d8bb88d3 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.8.2
 	github.com/xeipuuv/gojsonschema v1.2.0
 	golang.org/x/crypto v0.11.0
 	golang.org/x/term v0.10.0
diff --git a/go.sum b/go.sum
index f615f65c3..9affd8ed2 100644
--- a/go.sum
+++ b/go.sum
@@ -460,8 +460,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
diff --git a/pkg/action/action.go b/pkg/action/action.go
index 5693f4838..406765b45 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -103,7 +103,7 @@ type Configuration struct {
 // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
 //
 //	This code has to do with writing files to disk.
-func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
+func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
 	hs := []*release.Hook{}
 	b := bytes.NewBuffer(nil)
 
@@ -121,10 +121,12 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
 	var files map[string]string
 	var err2 error
 
-	// A `helm template` should not talk to the remote cluster. However, commands with the flag
-	//`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster.
-	// It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
-	if interactWithRemote && cfg.RESTClientGetter != nil {
+	// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
+	// It will break in interesting and exotic ways because other data (e.g. discovery)
+	// is mocked. It is not up to the template author to decide when the user wants to
+	// connect to the cluster. So when the user says to dry run, respect the user's
+	// wishes and do not connect to the cluster.
+	if !dryRun && cfg.RESTClientGetter != nil {
 		restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
 		if err != nil {
 			return hs, b, "", err
diff --git a/pkg/action/install.go b/pkg/action/install.go
index 1860b32f3..5fb7e6736 100644
--- a/pkg/action/install.go
+++ b/pkg/action/install.go
@@ -73,7 +73,6 @@ type Install struct {
 	Force                    bool
 	CreateNamespace          bool
 	DryRun                   bool
-	DryRunOption             string
 	DisableHooks             bool
 	Replace                  bool
 	Wait                     bool
@@ -233,16 +232,11 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 		return nil, err
 	}
 
-	var interactWithRemote bool
-	if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
-		interactWithRemote = true
-	}
-
 	// Pre-install anything in the crd/ directory. We do this before Helm
 	// contacts the upstream server and builds the capabilities object.
 	if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
 		// On dry run, bail here
-		if i.isDryRun() {
+		if i.DryRun {
 			i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
 		} else if err := i.installCRDs(crds); err != nil {
 			return nil, err
@@ -276,7 +270,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 	}
 
 	// special case for helm template --is-upgrade
-	isUpgrade := i.IsUpgrade && i.isDryRun()
+	isUpgrade := i.IsUpgrade && i.DryRun
 	options := chartutil.ReleaseOptions{
 		Name:      i.ReleaseName,
 		Namespace: i.Namespace,
@@ -292,7 +286,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 	rel := i.createRelease(chrt, vals)
 
 	var manifestDoc *bytes.Buffer
-	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS)
+	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun, i.EnableDNS)
 	// Even for errors, attach this if available
 	if manifestDoc != nil {
 		rel.Manifest = manifestDoc.String()
@@ -333,7 +327,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 	}
 
 	// Bail out here if it is a dry run
-	if i.isDryRun() {
+	if i.DryRun {
 		rel.Info.Description = "Dry run complete"
 		return rel, nil
 	}
@@ -393,14 +387,6 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 	}
 }
 
-// isDryRun returns true if Upgrade is set to run as a DryRun
-func (i *Install) isDryRun() bool {
-	if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
-		return true
-	}
-	return false
-}
-
 func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) {
 
 	// pre-install hooks
@@ -514,8 +500,7 @@ func (i *Install) availableName() error {
 	if err := chartutil.ValidateReleaseName(start); err != nil {
 		return errors.Wrapf(err, "release name %q", start)
 	}
-	// On dry run, bail here
-	if i.isDryRun() {
+	if i.DryRun {
 		return nil
 	}
 
diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go
index 8ee6ed881..784df4013 100644
--- a/pkg/action/upgrade.go
+++ b/pkg/action/upgrade.go
@@ -71,9 +71,8 @@ type Upgrade struct {
 	// DisableHooks disables hook processing if set to true.
 	DisableHooks bool
 	// DryRun controls whether the operation is prepared, but not executed.
+	// If `true`, the upgrade is prepared but not performed.
 	DryRun bool
-	// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
-	DryRunOption string
 	// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
 	//
 	// This should be used with caution.
@@ -148,7 +147,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
 	if err := chartutil.ValidateReleaseName(name); err != nil {
 		return nil, errors.Errorf("release name is invalid: %s", name)
 	}
-
 	u.cfg.Log("preparing upgrade for %s", name)
 	currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
 	if err != nil {
@@ -163,8 +161,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
 		return res, err
 	}
 
-	// Do not update for dry runs
-	if !u.isDryRun() {
+	if !u.DryRun {
 		u.cfg.Log("updating status for upgraded release for %s", name)
 		if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
 			return res, err
@@ -174,14 +171,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
 	return res, nil
 }
 
-// isDryRun returns true if Upgrade is set to run as a DryRun
-func (u *Upgrade) isDryRun() bool {
-	if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
-		return true
-	}
-	return false
-}
-
 // prepareUpgrade builds an upgraded release for an upgrade operation.
 func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
 	if chart == nil {
@@ -250,13 +239,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
 		return nil, nil, err
 	}
 
-	// Determine whether or not to interact with remote
-	var interactWithRemote bool
-	if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" {
-		interactWithRemote = true
-	}
-
-	hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS)
+	hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun, u.EnableDNS)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -334,8 +317,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
 		return nil
 	})
 
-	// Run if it is a dry run
-	if u.isDryRun() {
+	if u.DryRun {
 		u.cfg.Log("dry run for %s", upgradedRelease.Name)
 		if len(u.Description) > 0 {
 			upgradedRelease.Info.Description = u.Description
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index 509f82d4a..8ae1133f4 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -59,7 +59,7 @@ type (
 		out                io.Writer
 		authorizer         auth.Client
 		registryAuthorizer *registryauth.Client
-		resolver           remotes.Resolver
+		resolver           func(ref registry.Reference) (remotes.Resolver, error)
 		httpClient         *http.Client
 	}
 
@@ -86,9 +86,23 @@ func NewClient(options ...ClientOption) (*Client, error) {
 		}
 		client.authorizer = authClient
 	}
-	if client.resolver == nil {
+	client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
 		headers := http.Header{}
 		headers.Set("User-Agent", version.GetUserAgent())
+		dockerClient, ok := client.authorizer.(*dockerauth.Client)
+		if ok {
+			username, password, err := dockerClient.Credential(ref.Registry)
+			if err != nil {
+				return nil, errors.New("unable to retrieve credentials")
+			}
+			// A blank returned username and password value is a bearer token
+			if username == "" && password != "" {
+				headers.Set("Authorization", fmt.Sprintf("Bearer %s", password))
+			} else {
+				headers.Set("Authorization", fmt.Sprintf("Basic %s", basicAuth(username, password)))
+			}
+		}
+
 		opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
 		if client.httpClient != nil {
 			opts = append(opts, auth.WithResolverClient(client.httpClient))
@@ -97,9 +111,8 @@ func NewClient(options ...ClientOption) (*Client, error) {
 		if err != nil {
 			return nil, err
 		}
-		client.resolver = resolver
+		return resolver, nil
 	}
-
 	// allocate a cache if option is set
 	var cache registryauth.Cache
 	if client.enableCache {
@@ -117,7 +130,6 @@ func NewClient(options ...ClientOption) (*Client, error) {
 				if !ok {
 					return registryauth.EmptyCredential, errors.New("unable to obtain docker client")
 				}
-
 				username, password, err := dockerClient.Credential(reg)
 				if err != nil {
 					return registryauth.EmptyCredential, errors.New("unable to retrieve credentials")
@@ -324,7 +336,11 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
 	}
 
 	var descriptors, layers []ocispec.Descriptor
-	registryStore := content.Registry{Resolver: c.resolver}
+	remotesResolver, err := c.resolver(parsedRef)
+	if err != nil {
+		return nil, err
+	}
+	registryStore := content.Registry{Resolver: remotesResolver}
 
 	manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "",
 		oras.WithPullEmptyNameAllowed(),
@@ -562,8 +578,11 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
 	if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil {
 		return nil, err
 	}
-
-	registryStore := content.Registry{Resolver: c.resolver}
+	remotesResolver, err := c.resolver(parsedRef)
+	if err != nil {
+		return nil, err
+	}
+	registryStore := content.Registry{Resolver: remotesResolver}
 	_, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "",
 		oras.WithNameValidation(nil))
 	if err != nil {
diff --git a/pkg/registry/util.go b/pkg/registry/util.go
index 8baf0852a..ca93297e6 100644
--- a/pkg/registry/util.go
+++ b/pkg/registry/util.go
@@ -19,6 +19,7 @@ package registry // import "helm.sh/helm/v3/pkg/registry"
 import (
 	"bytes"
 	"context"
+	"encoding/base64"
 	"fmt"
 	"io"
 	"net/http"
@@ -245,3 +246,13 @@ func addToMap(inputMap map[string]string, newKey string, newValue string) map[st
 	return inputMap
 
 }
+
+// See 2 (end of page 4) https://www.ietf.org/rfc/rfc2617.txt
+// "To receive authorization, the client sends the userid and password,
+// separated by a single colon (":") character, within a base64
+// encoded string in the credentials."
+// It is not meant to be urlencoded.
+func basicAuth(username, password string) string {
+	auth := username + ":" + password
+	return base64.StdEncoding.EncodeToString([]byte(auth))
+}


Test patch:
diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go
index 5e3ae79c9..dd0cdb54d 100644
--- a/pkg/action/install_test.go
+++ b/pkg/action/install_test.go
@@ -254,7 +254,7 @@ func TestInstallRelease_DryRun(t *testing.T) {
 	is.Equal(res.Info.Description, "Dry run complete")
 }
 
-// Regression test for #7955
+// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run.
 func TestInstallRelease_DryRun_Lookup(t *testing.T) {
 	is := assert.New(t)
 	instAction := installAction(t)
diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go
index fdf09360b..f08c1fef1 100644
--- a/pkg/registry/util_test.go
+++ b/pkg/registry/util_test.go
@@ -238,3 +238,31 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
 	}
 
 }
+
+func Test_basicAuth(t *testing.T) {
+	type args struct {
+		username string
+		password string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "Basic Auth",
+			args: args{
+				username: "admin",
+				password: "passw0rd",
+			},
+			want: "YWRtaW46cGFzc3cwcmQ=",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := basicAuth(tt.args.username, tt.args.password); got != tt.want {
+				t.Errorf("basicAuth() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}