Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
k8s.io/apiextensions-apiserver v0.33.3
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.19.1
sigs.k8s.io/controller-tools v0.18.0
sigs.k8s.io/kustomize/api v0.19.0
Expand Down Expand Up @@ -198,7 +199,6 @@ require (
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/kubectl v0.33.3 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
Expand Down
137 changes: 117 additions & 20 deletions pkg/clients/helm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ limitations under the License.
package helm

import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"os"
"path"
Expand All @@ -34,6 +36,7 @@ import (
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"k8s.io/client-go/rest"
orasauth "oras.land/oras-go/v2/registry/remote/auth"
ktype "sigs.k8s.io/kustomize/api/types"

clusterv1beta1 "github.com/crossplane-contrib/provider-helm/apis/cluster/release/v1beta1"
Expand All @@ -47,14 +50,18 @@ const (
)

const (
errFailedToCheckIfLocalChartExists = "failed to check if cached chart file exists"
errFailedToPullChart = "failed to pull chart"
errFailedToLoadChart = "failed to load chart"
errUnexpectedDirContentTmpl = "expected 1 .tgz chart file, got [%s]"
errFailedToParseURL = "failed to parse URL"
errFailedToLogin = "failed to login to registry"
errUnexpectedOCIUrlTmpl = "url not prefixed with oci://, got [%s]"
devel = ">0.0.0-0"
errFailedToCheckIfLocalChartExists = "failed to check if cached chart file exists"
errFailedToPullChart = "failed to pull chart"
errFailedToLoadChart = "failed to load chart"
errUnexpectedDirContentTmpl = "expected 1 .tgz chart file, got [%s]"
errFailedToParseURL = "failed to parse URL"
errMissingOCIRegistryHostTmpl = "missing OCI registry host in url [%s]"
errFailedToCreateRegistryHTTPClient = "failed to create identity-token registry HTTP client"
errFailedToCreateRegistryClient = "failed to create identity-token registry client"
errUnexpectedRegistryTransportTmpl = "expected Helm registry transport base to be *http.Transport, got [%T]"
errFailedToLogin = "failed to login to registry"
errUnexpectedOCIUrlTmpl = "url not prefixed with oci://, got [%s]"
devel = ">0.0.0-0"
)

// Client is the interface to interact with Helm
Expand All @@ -76,6 +83,9 @@ type client struct {
rollbackClient *action.Rollback
uninstallClient *action.Uninstall
loginClient *action.RegistryLogin
// registryClient is the default registry client. pullChart resets to this
// before and after each pull so an identity-token-scoped client does not leak.
registryClient *registry.Client
}

// ArgsApplier defines helm client arguments helper
Expand Down Expand Up @@ -155,6 +165,7 @@ func NewClient(log logging.Logger, restConfig *rest.Config, argAppliers ...ArgsA
rollbackClient: rb,
uninstallClient: uic,
loginClient: lc,
registryClient: rc,
}, nil
}

Expand Down Expand Up @@ -203,6 +214,12 @@ func (hc *client) pullLatestChartVersion(chartUrl, chartName, chartVersion, char

func (hc *client) pullChart(chartUrl, chartName, chartVersion, chartRepo string, creds *RepoCreds, chartDir string) error {
pc := hc.pullClient
resetPullState(pc)
registryURL := chartUrl
if registryURL == "" {
registryURL = chartRepo
}
useIdentityToken := creds.hasIdentityToken() && registry.IsOCI(registryURL)

chartRef := chartUrl
if chartUrl == "" {
Expand All @@ -214,20 +231,29 @@ func (hc *client) pullChart(chartUrl, chartName, chartVersion, chartRepo string,
}
pc.Version = chartVersion
} else if registry.IsOCI(chartUrl) {
ociURL, version, err := resolveOCIChartVersion(chartUrl)
parsedURL, version, err := resolveOCIChartVersion(chartUrl)
if err != nil {
return err
}
pc.Version = version
chartRef = ociURL.String()
chartRef = parsedURL.String()
}
if creds.hasBasicAuth() && !useIdentityToken {
pc.Username = creds.Username
pc.Password = creds.Password
}
pc.Username = creds.Username
pc.Password = creds.Password

pc.DestDir = chartDir

if creds.Username != "" && creds.Password != "" {
err := hc.login(chartUrl, chartRepo, creds, pc.InsecureSkipTLSverify)
defer pc.SetRegistryClient(hc.registryClient)
if useIdentityToken {
if err := configureIdentityTokenRegistryClient(pc, registryURL, creds); err != nil {
return err
}
}

if creds.hasBasicAuth() && !useIdentityToken {
err := hc.login(registryURL, creds, pc.InsecureSkipTLSverify)
if err != nil {
return err
}
Expand All @@ -241,15 +267,86 @@ func (hc *client) pullChart(chartUrl, chartName, chartVersion, chartRepo string,
return nil
}

func (hc *client) login(chartUrl, chartRepo string, creds *RepoCreds, insecure bool) error {
ociURL := chartUrl
if chartUrl == "" {
ociURL = chartRepo
func configureIdentityTokenRegistryClient(pc *action.Pull, registryURL string, creds *RepoCreds) error {
parsedURL, err := url.Parse(registryURL)
if err != nil {
return errors.Wrap(err, errFailedToParseURL)
}
if parsedURL.Host == "" {
return errors.Errorf(errMissingOCIRegistryHostTmpl, registryURL)
}

rc, err := identityTokenRegistryClient(parsedURL.Host, creds, pc.InsecureSkipTLSverify, pc.PlainHTTP)
if err != nil {
return err
}
pc.SetRegistryClient(rc)
return nil
}

func resetPullState(pc *action.Pull) {
pc.Username = ""
pc.Password = ""
pc.Version = ""
pc.RepoURL = ""
}

func identityTokenRegistryClient(host string, creds *RepoCreds, insecureSkipTLSVerify, plainHTTP bool) (*registry.Client, error) {
httpClient, err := registryHTTPClient(insecureSkipTLSVerify)
if err != nil {
return nil, errors.Wrap(err, errFailedToCreateRegistryHTTPClient)
}

authorizer := orasauth.Client{
Client: httpClient,
Credential: orasauth.StaticCredential(host, creds.registryCredential()),
}

opts := []registry.ClientOption{
registry.ClientOptHTTPClient(httpClient),
registry.ClientOptAuthorizer(authorizer),
}
if !registry.IsOCI(ociURL) {
if plainHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}

rc, err := registry.NewClient(opts...)
if err != nil {
return nil, errors.Wrap(err, errFailedToCreateRegistryClient)
}
return rc, nil
}

func registryHTTPClient(insecureSkipTLSVerify bool) (*http.Client, error) {
// Keep Helm's normal retry transport. The flag disables Helm registry
// debug logging; it is unrelated to TLS verification.
transport := registry.NewTransport(false)
if !insecureSkipTLSVerify {
return &http.Client{Transport: transport}, nil
}

base, ok := transport.Base.(*http.Transport)
if !ok {
return nil, errors.Errorf(errUnexpectedRegistryTransportTmpl, transport.Base)
}

base = base.Clone()
if base.TLSClientConfig == nil {
base.TLSClientConfig = &tls.Config{} //nolint:gosec // This honors the Release's explicit insecureSkipTLSVerify setting.
} else {
base.TLSClientConfig = base.TLSClientConfig.Clone()
}
base.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec // This honors the Release's explicit insecureSkipTLSVerify setting.
transport.Base = base

return &http.Client{Transport: transport}, nil
}

func (hc *client) login(registryURL string, creds *RepoCreds, insecure bool) error {
if !registry.IsOCI(registryURL) {
return nil
}
parsedURL, err := url.Parse(ociURL)
parsedURL, err := url.Parse(registryURL)
if err != nil {
return errors.Wrap(err, errFailedToParseURL)
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/clients/helm/repocreds.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
package helm

import orasauth "oras.land/oras-go/v2/registry/remote/auth"

// RepoCreds keeps auth information to access a Helm Chart
type RepoCreds struct {
Username string
Password string

// IdentityToken is a Docker credential-helper identity token.
// ORAS models this value as a refresh token.
// For OCI chart pulls, identity-token credentials take precedence over
// Helm's basic-auth login path.
IdentityToken string
}

func (c *RepoCreds) hasBasicAuth() bool {
return c != nil && c.Username != "" && c.Password != ""
}

func (c *RepoCreds) hasIdentityToken() bool {
return c != nil && c.IdentityToken != ""
}

func (c *RepoCreds) registryCredential() orasauth.Credential {
if c == nil {
return orasauth.EmptyCredential
}

return orasauth.Credential{
Username: c.Username,
Password: c.Password,
RefreshToken: c.IdentityToken,
}
}
Loading