Repository: helm/helm (go)

Issue:
Improve OCI registry client configuration options for devs
Congrats on the 3.8 release! Happy to see OCI-related logic was extracted out of the experimental directory, meaning 3rd party developers can just import Helm and use it.

Precisely, in my team, we'd wanted to use the Helm OCI registry client to interact with OCI registries instead of dealing directly with ORAS. However, I find the current support (as of `v3.8.0`) quite limited.
More specifically:

- We'd need to be able to customize the resolver passed through the oras Registry, as we are adding a custom header and a preconfigured httpclient with the proper auth (not only _basic auth_ but also _bearer token_)

Currently, the only `ClientOption` are  `ClientOptDebug`, `ClientOptWriter` and `ClientOptCredentialsFile`, so adding a new one allowing customizing the resolver would make the trick.

Additionally, I'd like to have more `LoginOptions` as well. Currenlt only `LoginOptBasicAuth` and `LoginOptInsecure` are available.
As stated before, we'd like to customize other things like `userAgent` or `authorization` headers (or any other header) with arbitrary values.

Relevant file: https://github.com/helm/helm/blob/v3.8.0/pkg/registry/client.go

I don't know what's your roadmap on improving this kind of stuff, but willing to contribute if you want. 

Output of `helm version`: `version.BuildInfo{Version:"v3.8.0", GitCommit:"d14138609b01886f544b2025f5000351c9eb092e", GitTreeState:"clean", GoVersion:"go1.17.6"}`




Pull request:
Allow setting a resolver in the OCI registry client
**What this PR does / why we need it**:

As OCI was moved out of `/experimental`, third-party developers are using Helm 3.8 as their entrypoint to deal with OCI repositories (instead of dealing with ORAS directly). However, there is still some margin of improvement on the customization capabilities offered to those developers.

This PR is simply to add a way to enable devs to set up a custom containerd `resolver` for their OCI registry client.
Besides, it also exports a couple of fields, so that it becomes easier for devs to mock the `PullResult` struct in their unit tests.

closes #10623 (*)

(*) In that issue I mentioned some ways to improve the customization of the registry client, but, rather than adding a myriad of setters or ClientOpts for each aspect (custom headers, custom LoginOpts, etc.) setting a custom resolver makes the trick for us, at least.

**Special notes for your reviewer**:

After having reviewed the test suite for the client registry, I haven't found any existing code merely testing the ClientOpts, so I just added a `ClientOptResolver(nil)` to ensure the test coverage. Although the logic is trivial, please let me know if any other testing approach is required on your side.

I can confirm this change works for us: https://github.com/kubeapps/kubeapps/pull/4216



**If applicable**:
- [ ] this PR contains documentation
- [ ] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index decf853ce..0dfa6926f 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -87,7 +87,16 @@ func NewClient(options ...ClientOption) (*Client, error) {
 		}
 		client.authorizer = authClient
 	}
+
+	resolverFn := client.resolver // copy for avoiding recursive call
 	client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
+		if resolverFn != nil {
+			// validate if the resolverFn returns a valid resolver
+			if resolver, err := resolverFn(ref); resolver != nil && err == nil {
+				return resolver, nil
+			}
+		}
+
 		headers := http.Header{}
 		headers.Set("User-Agent", version.GetUserAgent())
 		dockerClient, ok := client.authorizer.(*dockerauth.Client)
@@ -117,6 +126,7 @@ func NewClient(options ...ClientOption) (*Client, error) {
 		}
 		return resolver, nil
 	}
+
 	// allocate a cache if option is set
 	var cache registryauth.Cache
 	if client.enableCache {
@@ -199,6 +209,15 @@ func ClientOptPlainHTTP() ClientOption {
 	}
 }
 
+// ClientOptResolver returns a function that sets the resolver setting on a client options set
+func ClientOptResolver(resolver remotes.Resolver) ClientOption {
+	return func(client *Client) {
+		client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
+			return resolver, nil
+		}
+	}
+}
+
 type (
 	// LoginOption allows specifying various settings on login
 	LoginOption func(*loginOperation)
@@ -287,21 +306,21 @@ type (
 
 	// 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"`
+		Manifest *DescriptorPullSummary         `json:"manifest"`
+		Config   *DescriptorPullSummary         `json:"config"`
+		Chart    *DescriptorPullSummaryWithMeta `json:"chart"`
+		Prov     *DescriptorPullSummary         `json:"prov"`
 		Ref      string                         `json:"ref"`
 	}
 
-	descriptorPullSummary struct {
+	DescriptorPullSummary struct {
 		Data   []byte `json:"-"`
 		Digest string `json:"digest"`
 		Size   int64  `json:"size"`
 	}
 
-	descriptorPullSummaryWithMeta struct {
-		descriptorPullSummary
+	DescriptorPullSummaryWithMeta struct {
+		DescriptorPullSummary
 		Meta *chart.Metadata `json:"meta"`
 	}
 
@@ -404,16 +423,16 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
 		}
 	}
 	result := &PullResult{
-		Manifest: &descriptorPullSummary{
+		Manifest: &DescriptorPullSummary{
 			Digest: manifest.Digest.String(),
 			Size:   manifest.Size,
 		},
-		Config: &descriptorPullSummary{
+		Config: &DescriptorPullSummary{
 			Digest: configDescriptor.Digest.String(),
 			Size:   configDescriptor.Size,
 		},
-		Chart: &descriptorPullSummaryWithMeta{},
-		Prov:  &descriptorPullSummary{},
+		Chart: &DescriptorPullSummaryWithMeta{},
+		Prov:  &DescriptorPullSummary{},
 		Ref:   parsedRef.String(),
 	}
 	var getManifestErr error


Test patch:
diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go
index 72a614f6d..74aa0dbc0 100644
--- a/pkg/registry/utils_test.go
+++ b/pkg/registry/utils_test.go
@@ -88,6 +88,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry {
 		ClientOptEnableCache(true),
 		ClientOptWriter(suite.Out),
 		ClientOptCredentialsFile(credentialsFile),
+		ClientOptResolver(nil),
 	}
 
 	if tlsEnabled {