Refactor to use authn for authentication as OCIrepository does

If implemented the oras registry loginOption will only be used internaly
with the specific ChartRepo struct.

This will permit reusing more easily feature developped with
googlecontainerregistry authn.

Signed-off-by: Soule BA <soule@weave.works>
This commit is contained in:
Soule BA 2022-09-26 16:10:53 +02:00
parent d372531204
commit bb83270acc
No known key found for this signature in database
GPG Key ID: 4D40965192802994
5 changed files with 185 additions and 97 deletions

View File

@ -56,6 +56,7 @@ import (
"github.com/fluxcd/pkg/runtime/patch" "github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/pkg/untar" "github.com/fluxcd/pkg/untar"
"github.com/google/go-containerregistry/pkg/authn"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/source-controller/internal/cache" "github.com/fluxcd/source-controller/internal/cache"
@ -455,8 +456,9 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *sourcev1.HelmChart, func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *sourcev1.HelmChart,
repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) { repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
loginOpts []helmreg.LoginOption authenticator authn.Authenticator
keychain authn.Keychain
) )
// Used to login with the repository declared provider // Used to login with the repository declared provider
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration) ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
@ -481,10 +483,10 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
} }
// Build client options from secret // Build client options from secret
opts, err := getter.ClientOptionsFromSecret(*secret) opts, tls, err := r.clientOptionsFromSecret(secret, normalizedURL)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err), Err: err,
Reason: sourcev1.AuthenticationFailedReason, Reason: sourcev1.AuthenticationFailedReason,
} }
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
@ -492,20 +494,10 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
clientOpts = append(clientOpts, opts...) clientOpts = append(clientOpts, opts...)
tlsConfig = tls
tlsConfig, err = getter.TLSClientConfigFromSecret(*secret, normalizedURL)
if err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to create TLS client config with secret data: %w", err),
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
// Requeue as content of secret might change
return sreconcile.ResultEmpty, e
}
// Build registryClient options from secret // Build registryClient options from secret
loginOpt, err := registry.LoginOptionFromSecret(normalizedURL, *secret) keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err), Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@ -515,10 +507,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
// Requeue as content of secret might change // Requeue as content of secret might change
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { } else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider) auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr), Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
@ -528,10 +518,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
if auth != nil { if auth != nil {
loginOpts = append([]helmreg.LoginOption{}, auth) authenticator = auth
} }
} }
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
if err != nil {
e := &serror.Event{
Err: err,
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
// Initialize the chart repository // Initialize the chart repository
var chartRepo repository.Downloader var chartRepo repository.Downloader
switch repo.Spec.Type { switch repo.Spec.Type {
@ -545,7 +545,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
// this is needed because otherwise the credentials are stored in ~/.docker/config.json. // this is needed because otherwise the credentials are stored in ~/.docker/config.json.
// TODO@souleb: remove this once the registry move to Oras v2 // TODO@souleb: remove this once the registry move to Oras v2
// or rework to enable reusing credentials to avoid the unneccessary handshake operations // or rework to enable reusing credentials to avoid the unneccessary handshake operations
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil) registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to construct Helm client: %w", err), Err: fmt.Errorf("failed to construct Helm client: %w", err),
@ -574,8 +574,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
// If login options are configured, use them to login to the registry // If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart // The OCIGetter will later retrieve the stored credentials to pull the chart
if loginOpts != nil { if keychain != nil {
err = ociChartRepo.Login(loginOpts...) err = ociChartRepo.Login(loginOpt)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to login to OCI registry: %w", err), Err: fmt.Errorf("failed to login to OCI registry: %w", err),
@ -941,8 +941,9 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *sourcev1.
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback { func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
return func(url string) (repository.Downloader, error) { return func(url string) (repository.Downloader, error) {
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
loginOpts []helmreg.LoginOption authenticator authn.Authenticator
keychain authn.Keychain
) )
normalizedURL := repository.NormalizeURL(url) normalizedURL := repository.NormalizeURL(url)
repo, err := r.resolveDependencyRepository(ctx, url, namespace) repo, err := r.resolveDependencyRepository(ctx, url, namespace)
@ -972,37 +973,39 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
if err != nil { if err != nil {
return nil, err return nil, err
} }
opts, err := getter.ClientOptionsFromSecret(*secret)
// Build client options from secret
opts, tls, err := r.clientOptionsFromSecret(secret, normalizedURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
clientOpts = append(clientOpts, opts...) clientOpts = append(clientOpts, opts...)
tlsConfig = tls
tlsConfig, err = getter.TLSClientConfigFromSecret(*secret, normalizedURL)
if err != nil {
return nil, fmt.Errorf("failed to create TLS client config for HelmRepository '%s': %w", repo.Name, err)
}
// Build registryClient options from secret // Build registryClient options from secret
loginOpt, err := registry.LoginOptionFromSecret(normalizedURL, *secret) keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", repo.Name, err) return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", repo.Name, err)
} }
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { } else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider) auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr) return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
} }
if auth != nil { if auth != nil {
loginOpts = append([]helmreg.LoginOption{}, auth) authenticator = auth
} }
} }
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
if err != nil {
return nil, err
}
var chartRepo repository.Downloader var chartRepo repository.Downloader
if helmreg.IsOCI(normalizedURL) { if helmreg.IsOCI(normalizedURL) {
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil) registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", repo.Name, err) return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", repo.Name, err)
} }
@ -1027,8 +1030,8 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
// If login options are configured, use them to login to the registry // If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart // The OCIGetter will later retrieve the stored credentials to pull the chart
if loginOpts != nil { if keychain != nil {
err = ociChartRepo.Login(loginOpts...) err = ociChartRepo.Login(loginOpt)
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err)) errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
// clean up the credentialsFile // clean up the credentialsFile
@ -1078,6 +1081,20 @@ func (r *HelmChartReconciler) resolveDependencyRepository(ctx context.Context, u
return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace) return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace)
} }
func (r *HelmChartReconciler) clientOptionsFromSecret(secret *corev1.Secret, normalizedURL string) ([]helmgetter.Option, *tls.Config, error) {
opts, err := getter.ClientOptionsFromSecret(*secret)
if err != nil {
return nil, nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
}
tlsConfig, err := getter.TLSClientConfigFromSecret(*secret, normalizedURL)
if err != nil {
return nil, nil, fmt.Errorf("failed to create TLS client config with secret data: %w", err)
}
return opts, tlsConfig, nil
}
func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *sourcev1.HelmRepository) (*corev1.Secret, error) { func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *sourcev1.HelmRepository) (*corev1.Secret, error) {
if repository.Spec.SecretRef == nil { if repository.Spec.SecretRef == nil {
return nil, nil return nil, nil

View File

@ -45,6 +45,7 @@ import (
helper "github.com/fluxcd/pkg/runtime/controller" helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/patch" "github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/predicates"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/fluxcd/source-controller/api/v1beta2" "github.com/fluxcd/source-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@ -263,36 +264,21 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
} }
conditions.Delete(obj, meta.StalledCondition) conditions.Delete(obj, meta.StalledCondition)
var loginOpts []helmreg.LoginOption var (
authenticator authn.Authenticator
keychain authn.Keychain
err error
)
// Configure any authentication related options. // Configure any authentication related options.
if obj.Spec.SecretRef != nil { if obj.Spec.SecretRef != nil {
// Attempt to retrieve secret. keychain, err = authFromSecret(ctx, r.Client, obj)
name := types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.Spec.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Client.Get(ctx, name, &secret); err != nil {
e := fmt.Errorf("failed to get secret '%s': %w", name.String(), err)
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
result, retErr = ctrl.Result{}, e
return
}
// Construct login options.
loginOpt, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
if err != nil { if err != nil {
e := fmt.Errorf("failed to configure Helm client with secret data: %w", err) conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error()) result, retErr = ctrl.Result{}, err
result, retErr = ctrl.Result{}, e
return return
} }
if loginOpt != nil {
loginOpts = append(loginOpts, loginOpt)
}
} else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { } else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuthFromAdapter(ctxTimeout, obj.Spec.URL, obj.Spec.Provider) auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr) e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error()) conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
@ -300,12 +286,19 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
return return
} }
if auth != nil { if auth != nil {
loginOpts = append(loginOpts, auth) authenticator = auth
} }
} }
loginOpt, err := makeLoginOption(authenticator, keychain, obj.Spec.URL)
if err != nil {
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
result, retErr = ctrl.Result{}, err
return
}
// Create registry client and login if needed. // Create registry client and login if needed.
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil) registryClient, file, err := r.RegistryClientGenerator(loginOpt != nil)
if err != nil { if err != nil {
e := fmt.Errorf("failed to create registry client: %w", err) e := fmt.Errorf("failed to create registry client: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error()) conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
@ -332,8 +325,8 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
conditions.Delete(obj, meta.StalledCondition) conditions.Delete(obj, meta.StalledCondition)
// Attempt to login to the registry if credentials are provided. // Attempt to login to the registry if credentials are provided.
if loginOpts != nil { if loginOpt != nil {
err = chartRepo.Login(loginOpts...) err = chartRepo.Login(loginOpt)
if err != nil { if err != nil {
e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err) e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error()) conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
@ -375,16 +368,37 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
r.Eventf(obj, eventType, reason, msg) r.Eventf(obj, eventType, reason, msg)
} }
// oidcAuthFromAdapter generates the OIDC credential authenticator based on the specified cloud provider. // authFromSecret returns an authn.Keychain for the given HelmRepository.
func oidcAuthFromAdapter(ctx context.Context, url, provider string) (helmreg.LoginOption, error) { // If the HelmRepository does not specify a secretRef, an anonymous keychain is returned.
auth, err := oidcAuth(ctx, url, provider) func authFromSecret(ctx context.Context, client client.Client, obj *sourcev1.HelmRepository) (authn.Keychain, error) {
// Attempt to retrieve secret.
name := types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.Spec.SecretRef.Name,
}
var secret corev1.Secret
if err := client.Get(ctx, name, &secret); err != nil {
return nil, fmt.Errorf("failed to get secret '%s': %w", name.String(), err)
}
// Construct login options.
keychain, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
} }
return keychain, nil
if auth == nil { }
return nil, fmt.Errorf("could not validate OCI provider %s with URL %s", provider, url)
} // makeLoginOption returns a registry login option for the given HelmRepository.
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
return registry.OIDCAdaptHelper(auth) func makeLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (helmreg.LoginOption, error) {
if auth != nil {
return registry.AuthAdaptHelper(auth)
}
if keychain != nil {
return registry.KeychainAdaptHelper(keychain)(registryURL)
}
return nil, nil
} }

View File

@ -23,27 +23,42 @@ import (
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
"github.com/fluxcd/source-controller/internal/oci"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
"helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
) )
// helper is a subset of the Docker credential helper credentials.Helper interface used by NewKeychainFromHelper.
type helper struct {
registry string
username, password string
err error
}
func (h helper) Get(serverURL string) (string, string, error) {
if serverURL != h.registry {
return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL)
}
return h.username, h.password, h.err
}
// LoginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret // LoginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold // may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are // a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
// empty, a nil LoginOption and a nil error will be returned. // empty, a nil LoginOption and a nil error will be returned.
func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.LoginOption, error) { func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (authn.Keychain, error) {
var username, password string var username, password string
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL '%s' while reconciling Secret '%s': %w",
registryURL, secret.Name, err)
}
if secret.Type == corev1.SecretTypeDockerConfigJson { if secret.Type == corev1.SecretTypeDockerConfigJson {
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey])) dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load Docker config from Secret '%s': %w", secret.Name, err) return nil, fmt.Errorf("unable to load Docker config from Secret '%s': %w", secret.Name, err)
} }
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL '%s' while reconciling Secret '%s': %w",
registryURL, secret.Name, err)
}
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host) authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get authentication data from Secret '%s': %w", secret.Name, err) return nil, fmt.Errorf("unable to get authentication data from Secret '%s': %w", secret.Name, err)
@ -63,19 +78,38 @@ func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.L
} }
switch { switch {
case username == "" && password == "": case username == "" && password == "":
return nil, nil return oci.Anonymous{}, nil
case username == "" || password == "": case username == "" || password == "":
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name) return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
} }
return registry.LoginOptBasicAuth(username, password), nil return authn.NewKeychainFromHelper(helper{registry: parsedURL.Host, username: username, password: password}), nil
} }
// OIDCAdaptHelper returns an ORAS credentials callback configured with the authorization data // KeyChainAdaptHelper returns an ORAS credentials callback configured with the authorization data
// from the given authn authenticator. This allows for example to make use of credential helpers from // from the given authn keychain. This allows for example to make use of credential helpers from
// cloud providers. // cloud providers.
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn // Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, error) { func KeychainAdaptHelper(keyChain authn.Keychain) func(string) (registry.LoginOption, error) {
authConfig, err := authenticator.Authorization() return func(registryURL string) (registry.LoginOption, error) {
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL '%s'", registryURL)
}
authenticator, err := keyChain.Resolve(resource{parsedURL.Host})
if err != nil {
return nil, fmt.Errorf("unable to resolve credentials for registry '%s': %w", registryURL, err)
}
return AuthAdaptHelper(authenticator)
}
}
// AuthAdaptHelper returns an ORAS credentials callback configured with the authorization data
// from the given authn authenticator This allows for example to make use of credential helpers from
// cloud providers.
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
func AuthAdaptHelper(auth authn.Authenticator) (registry.LoginOption, error) {
authConfig, err := auth.Authorization()
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err) return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err)
} }
@ -91,3 +125,15 @@ func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, e
} }
return registry.LoginOptBasicAuth(username, password), nil return registry.LoginOptBasicAuth(username, password), nil
} }
type resource struct {
registry string
}
func (r resource) String() string {
return r.registry
}
func (r resource) RegistryStr() string {
return r.registry
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package registry package registry
import ( import (
"net/url"
"testing" "testing"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
@ -24,6 +25,8 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
) )
const repoURL = "https://example.com"
func TestLoginOptionFromSecret(t *testing.T) { func TestLoginOptionFromSecret(t *testing.T) {
testURL := "oci://registry.example.com/foo/bar" testURL := "oci://registry.example.com/foo/bar"
testUser := "flux" testUser := "flux"
@ -131,33 +134,40 @@ func TestLoginOptionFromSecret(t *testing.T) {
} }
} }
func TestOIDCAdaptHelper(t *testing.T) { func TestKeychainAdaptHelper(t *testing.T) {
auth := &authn.Basic{ g := NewWithT(t)
Username: "flux", reg, err := url.Parse(repoURL)
Password: "flux_password", if err != nil {
g.Expect(err).ToNot(HaveOccurred())
}
auth := helper{
username: "flux",
password: "flux_password",
registry: reg.Host,
} }
tests := []struct { tests := []struct {
name string name string
auth authn.Authenticator auth authn.Keychain
expectedLogin bool expectedLogin bool
wantErr bool wantErr bool
}{ }{
{ {
name: "Login from basic auth with empty auth", name: "Login from basic auth with empty auth",
auth: &authn.Basic{}, auth: authn.NewKeychainFromHelper(helper{}),
expectedLogin: false, expectedLogin: false,
wantErr: false, wantErr: false,
}, },
{ {
name: "Login from basic auth", name: "Login from basic auth",
auth: auth, auth: authn.NewKeychainFromHelper(auth),
expectedLogin: true, expectedLogin: true,
wantErr: false, wantErr: false,
}, },
{ {
name: "Login with missing password", name: "Login with missing password",
auth: &authn.Basic{Username: "flux"}, auth: authn.NewKeychainFromHelper(helper{username: "flux", registry: reg.Host}),
expectedLogin: false, expectedLogin: false,
wantErr: true, wantErr: true,
}, },
@ -166,7 +176,7 @@ func TestOIDCAdaptHelper(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
loginOpt, err := OIDCAdaptHelper(tt.auth) loginOpt, err := KeychainAdaptHelper(tt.auth)(repoURL)
if tt.wantErr { if tt.wantErr {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
return return

View File

@ -215,6 +215,7 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
// Login attempts to login to the OCI registry. // Login attempts to login to the OCI registry.
// It returns an error on failure. // It returns an error on failure.
func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error { func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
// Get login credentials from keychain
err := r.RegistryClient.Login(r.URL.Host, opts...) err := r.RegistryClient.Login(r.URL.Host, opts...)
if err != nil { if err != nil {
return err return err