Merge pull request #1160 from fluxcd/helm-cert-secret

helmrepo: add `.spec.certSecretRef` for specifying TLS auth data
This commit is contained in:
Sanskar Jaiswal 2023-07-31 13:50:00 +05:30 committed by GitHub
commit 3840940354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 669 additions and 527 deletions

View File

@ -51,11 +51,18 @@ type HelmRepositorySpec struct {
// for the HelmRepository. // for the HelmRepository.
// For HTTP/S basic auth the secret must contain 'username' and 'password' // For HTTP/S basic auth the secret must contain 'username' and 'password'
// fields. // fields.
// For TLS the secret must contain a 'certFile' and 'keyFile', and/or // Support for TLS auth using the 'certFile' and 'keyFile', and/or 'caFile'
// 'caFile' fields. // keys is deprecated. Please use `.spec.certSecretRef` instead.
// +optional // +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
// CertSecretRef specifies the Secret containing the TLS authentication
// data. The secret must contain a 'certFile' and 'keyFile', and/or 'caFile'
// fields. It takes precedence over the values specified in the Secret
// referred to by `.spec.secretRef`.
// +optional
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
// PassCredentials allows the credentials from the SecretRef to be passed // PassCredentials allows the credentials from the SecretRef to be passed
// on to a host that does not match the host as defined in URL. // on to a host that does not match the host as defined in URL.
// This may be required if the host of the advertised chart URLs in the // This may be required if the host of the advertised chart URLs in the

View File

@ -577,6 +577,11 @@ func (in *HelmRepositorySpec) DeepCopyInto(out *HelmRepositorySpec) {
*out = new(meta.LocalObjectReference) *out = new(meta.LocalObjectReference)
**out = **in **out = **in
} }
if in.CertSecretRef != nil {
in, out := &in.CertSecretRef, &out.CertSecretRef
*out = new(meta.LocalObjectReference)
**out = **in
}
out.Interval = in.Interval out.Interval = in.Interval
if in.Timeout != nil { if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout in, out := &in.Timeout, &out.Timeout

View File

@ -296,6 +296,18 @@ spec:
required: required:
- namespaceSelectors - namespaceSelectors
type: object type: object
certSecretRef:
description: CertSecretRef specifies the Secret containing the TLS
authentication data. The secret must contain a 'certFile' and 'keyFile',
and/or 'caFile' fields. It takes precedence over the values specified
in the Secret referred to by `.spec.secretRef`.
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
interval: interval:
description: Interval at which to check the URL for updates. description: Interval at which to check the URL for updates.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
@ -323,8 +335,9 @@ spec:
secretRef: secretRef:
description: SecretRef specifies the Secret containing authentication description: SecretRef specifies the Secret containing authentication
credentials for the HelmRepository. For HTTP/S basic auth the secret credentials for the HelmRepository. For HTTP/S basic auth the secret
must contain 'username' and 'password' fields. For TLS the secret must contain 'username' and 'password' fields. Support for TLS auth
must contain a 'certFile' and 'keyFile', and/or 'caFile' fields. using the 'certFile' and 'keyFile', and/or 'caFile' keys is deprecated.
Please use `.spec.certSecretRef` instead.
properties: properties:
name: name:
description: Name of the referent. description: Name of the referent.

View File

@ -792,8 +792,25 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
for the HelmRepository. for the HelmRepository.
For HTTP/S basic auth the secret must contain ‘username’ and ‘password’ For HTTP/S basic auth the secret must contain ‘username’ and ‘password’
fields. fields.
For TLS the secret must contain a ‘certFile’ and ‘keyFile’, and/or Support for TLS auth using the ‘certFile’ and ‘keyFile’, and/or ‘caFile’
&lsquo;caFile&rsquo; fields.</p> keys is deprecated. Please use <code>.spec.certSecretRef</code> instead.</p>
</td>
</tr>
<tr>
<td>
<code>certSecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing the TLS authentication
data. The secret must contain a &lsquo;certFile&rsquo; and &lsquo;keyFile&rsquo;, and/or &lsquo;caFile&rsquo;
fields. It takes precedence over the values specified in the Secret
referred to by <code>.spec.secretRef</code>.</p>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -2459,8 +2476,25 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
for the HelmRepository. for the HelmRepository.
For HTTP/S basic auth the secret must contain &lsquo;username&rsquo; and &lsquo;password&rsquo; For HTTP/S basic auth the secret must contain &lsquo;username&rsquo; and &lsquo;password&rsquo;
fields. fields.
For TLS the secret must contain a &lsquo;certFile&rsquo; and &lsquo;keyFile&rsquo;, and/or Support for TLS auth using the &lsquo;certFile&rsquo; and &lsquo;keyFile&rsquo;, and/or &lsquo;caFile&rsquo;
&lsquo;caFile&rsquo; fields.</p> keys is deprecated. Please use <code>.spec.certSecretRef</code> instead.</p>
</td>
</tr>
<tr>
<td>
<code>certSecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing the TLS authentication
data. The secret must contain a &lsquo;certFile&rsquo; and &lsquo;keyFile&rsquo;, and/or &lsquo;caFile&rsquo;
fields. It takes precedence over the values specified in the Secret
referred to by <code>.spec.secretRef</code>.</p>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -452,15 +452,37 @@ flux create secret oci ghcr-auth \
--password=${GITHUB_PAT} --password=${GITHUB_PAT}
``` ```
#### TLS authentication **Note:** Support for specifying TLS authentication data using this API has been
deprecated. Please use [`.spec.certSecretRef`](#cert-secret-reference) instead.
If the controller uses the secret specfied by this field to configure TLS, then
a deprecation warning will be logged.
### Cert secret reference
**Note:** TLS authentication is not yet supported by OCI Helm repositories. **Note:** TLS authentication is not yet supported by OCI Helm repositories.
To provide TLS credentials to use while connecting with the Helm repository, `.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
the referenced Secret is expected to contain `.data.certFile` and certificate data. The secret can contain the following keys:
`.data.keyFile`, and/or `.data.caFile` values.
For example: * `certFile` and `keyFile`, to specify the client certificate and private key used for
TLS client authentication. These must be used in conjunction, i.e. specifying one without
the other will lead to an error.
* `caFile`, to specify the CA certificate used to verify the server, which is required
if the server is using a self-signed certificate.
If the server is using a self-signed certificate and has TLS client authentication enabled,
all three values are required.
All the files in the secret are expected to be [PEM-encoded][pem-encoding]. Assuming you have
three files; `client.key`, `client.crt` and `ca.crt` for the client private key, client
certificate and the CA certificate respectively, you can generate the required secret using
the `flux creat secret helm` command:
```sh
flux create secret helm tls --key-file=client.key --cert-file=client.crt --ca-file=ca.crt
```
Example usage:
```yaml ```yaml
--- ---
@ -472,7 +494,7 @@ metadata:
spec: spec:
interval: 5m0s interval: 5m0s
url: https://example.com url: https://example.com
secretRef: certSecretRef:
name: example-tls name: example-tls
--- ---
apiVersion: v1 apiVersion: v1

View File

@ -18,7 +18,6 @@ package controller
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@ -28,7 +27,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
helmgetter "helm.sh/helm/v3/pkg/getter" helmgetter "helm.sh/helm/v3/pkg/getter"
@ -54,7 +52,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/conditions"
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"
@ -68,7 +65,6 @@ import (
serror "github.com/fluxcd/source-controller/internal/error" serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/helm/chart" "github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/getter" "github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/registry"
"github.com/fluxcd/source-controller/internal/helm/repository" "github.com/fluxcd/source-controller/internal/helm/repository"
soci "github.com/fluxcd/source-controller/internal/oci" soci "github.com/fluxcd/source-controller/internal/oci"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile" sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
@ -506,11 +502,6 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, sp *patch.Ser
// object, and returns early. // object, and returns early.
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *helmv1.HelmChart, func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *helmv1.HelmChart,
repo *helmv1.HelmRepository, b *chart.Build) (sreconcile.Result, error) { repo *helmv1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
var (
tlsConfig *tls.Config
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)
defer cancel() defer cancel()
@ -519,65 +510,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
if err != nil { if err != nil {
return chartRepoConfigErrorReturn(err, obj) return chartRepoConfigErrorReturn(err, obj)
} }
// Construct the Getter options from the HelmRepository data clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
clientOpts := []helmgetter.Option{ if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
helmgetter.WithURL(normalizedURL),
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
helmgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
}
if secret, err := r.getHelmRepositorySecret(ctx, repo); secret != nil || err != nil {
if err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to get secret '%s': %w", repo.Spec.SecretRef.Name, err),
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
// Return error as the world as observed may change
return sreconcile.ResultEmpty, e
}
// Build client options from secret
opts, tlsCfg, err := r.clientOptionsFromSecret(secret, normalizedURL)
if err != nil {
e := &serror.Event{
Err: 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
}
clientOpts = append(clientOpts, opts...)
tlsConfig = tlsCfg
// Build registryClient options from secret
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
if err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client 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
}
} else if repo.Spec.Provider != helmv1.GenericOCIProvider && repo.Spec.Type == helmv1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
e := &serror.Event{
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
if auth != nil {
authenticator = auth
}
}
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: err, Err: err,
Reason: sourcev1.AuthenticationFailedReason, Reason: sourcev1.AuthenticationFailedReason,
@ -585,6 +519,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
getterOpts := clientOpts.GetterOpts
// Initialize the chart repository // Initialize the chart repository
var chartRepo repository.Downloader var chartRepo repository.Downloader
@ -599,7 +534,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(loginOpt != nil) registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != 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),
@ -621,7 +556,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
var verifiers []soci.Verifier var verifiers []soci.Verifier
if obj.Spec.Verify != nil { if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider provider := obj.Spec.Verify.Provider
verifiers, err = r.makeVerifiers(ctx, obj, authenticator, keychain) verifiers, err = r.makeVerifiers(ctx, obj, *clientOpts)
if err != nil { if err != nil {
if obj.Spec.Verify.SecretRef == nil { if obj.Spec.Verify.SecretRef == nil {
provider = fmt.Sprintf("%s keyless", provider) provider = fmt.Sprintf("%s keyless", provider)
@ -636,21 +571,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
} }
// Tell the chart repository to use the OCI client with the configured getter // Tell the chart repository to use the OCI client with the configured getter
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient)) getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
repository.WithOCIGetter(r.Getters), repository.WithOCIGetter(r.Getters),
repository.WithOCIGetterOptions(clientOpts), repository.WithOCIGetterOptions(getterOpts),
repository.WithOCIRegistryClient(registryClient), repository.WithOCIRegistryClient(registryClient),
repository.WithVerifiers(verifiers)) repository.WithVerifiers(verifiers))
if err != nil { if err != nil {
return chartRepoConfigErrorReturn(err, obj) return chartRepoConfigErrorReturn(err, obj)
} }
chartRepo = ociChartRepo
// 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 loginOpt != nil { if clientOpts.RegLoginOpt != nil {
err = ociChartRepo.Login(loginOpt) err = ociChartRepo.Login(clientOpts.RegLoginOpt)
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),
@ -660,8 +594,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
} }
chartRepo = ociChartRepo
default: default:
httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts...) httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, clientOpts.TlsConfig, getterOpts...)
if err != nil { if err != nil {
return chartRepoConfigErrorReturn(err, obj) return chartRepoConfigErrorReturn(err, obj)
} }
@ -1024,12 +959,6 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *helmv1.He
// The callback returns an object with a state, so the caller has to do the necessary cleanup. // The callback returns an object with a state, so the caller has to do the necessary cleanup.
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 (
tlsConfig *tls.Config
authenticator authn.Authenticator
keychain authn.Keychain
)
normalizedURL, err := repository.NormalizeURL(url) normalizedURL, err := repository.NormalizeURL(url)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1052,61 +981,28 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel() defer cancel()
clientOpts := []helmgetter.Option{ clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
helmgetter.WithURL(normalizedURL), if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
}
if secret, err := r.getHelmRepositorySecret(ctx, obj); secret != nil || err != nil {
if err != nil {
return nil, err
}
// Build client options from secret
opts, tlsCfg, err := r.clientOptionsFromSecret(secret, normalizedURL)
if err != nil {
return nil, err
}
clientOpts = append(clientOpts, opts...)
tlsConfig = tlsCfg
// Build registryClient options from secret
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
if err != nil {
return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", obj.Name, err)
}
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
return nil, fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
}
if auth != nil {
authenticator = auth
}
}
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
if err != nil {
return nil, err return nil, err
} }
getterOpts := clientOpts.GetterOpts
var chartRepo repository.Downloader var chartRepo repository.Downloader
if helmreg.IsOCI(normalizedURL) { if helmreg.IsOCI(normalizedURL) {
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil) registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", obj.Name, err) return nil, fmt.Errorf("failed to create registry client: %w", err)
} }
var errs []error var errs []error
// Tell the chart repository to use the OCI client with the configured getter // Tell the chart repository to use the OCI client with the configured getter
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient)) getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters), ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
repository.WithOCIGetterOptions(clientOpts), repository.WithOCIGetterOptions(getterOpts),
repository.WithOCIRegistryClient(registryClient), repository.WithOCIRegistryClient(registryClient),
repository.WithCredentialsFile(credentialsFile)) repository.WithCredentialsFile(credentialsFile))
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("failed to create OCI chart repository for HelmRepository '%s': %w", obj.Name, err)) errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
// clean up the credentialsFile // clean up the credentialsFile
if credentialsFile != "" { if credentialsFile != "" {
if err := os.Remove(credentialsFile); err != nil { if err := os.Remove(credentialsFile); err != nil {
@ -1118,10 +1014,10 @@ 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 loginOpt != nil { if clientOpts.RegLoginOpt != nil {
err = ociChartRepo.Login(loginOpt) err = ociChartRepo.Login(clientOpts.RegLoginOpt)
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", obj.Name, err)) errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
// clean up the credentialsFile // clean up the credentialsFile
errs = append(errs, ociChartRepo.Clear()) errs = append(errs, ociChartRepo.Clear())
return nil, kerrors.NewAggregate(errs) return nil, kerrors.NewAggregate(errs)
@ -1130,7 +1026,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
chartRepo = ociChartRepo chartRepo = ociChartRepo
} else { } else {
httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, tlsConfig, clientOpts...) httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, clientOpts.TlsConfig, getterOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1178,36 +1074,6 @@ 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 *helmv1.HelmRepository) (*corev1.Secret, error) {
if repository.Spec.SecretRef == nil {
return nil, nil
}
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
var secret corev1.Secret
err := r.Client.Get(ctx, name, &secret)
if err != nil {
return nil, err
}
return &secret, nil
}
func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string { func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string {
repo, ok := o.(*helmv1.HelmRepository) repo, ok := o.(*helmv1.HelmRepository)
if !ok { if !ok {
@ -1412,13 +1278,14 @@ func chartRepoConfigErrorReturn(err error, obj *helmv1.HelmChart) (sreconcile.Re
} }
// makeVerifiers returns a list of verifiers for the given chart. // makeVerifiers returns a list of verifiers for the given chart.
func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) ([]soci.Verifier, error) { func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.HelmChart, clientOpts getter.ClientOpts) ([]soci.Verifier, error) {
var verifiers []soci.Verifier var verifiers []soci.Verifier
verifyOpts := []remote.Option{} verifyOpts := []remote.Option{}
if auth != nil {
verifyOpts = append(verifyOpts, remote.WithAuth(auth)) if clientOpts.Authenticator != nil {
verifyOpts = append(verifyOpts, remote.WithAuth(clientOpts.Authenticator))
} else { } else {
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain)) verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(clientOpts.Keychain))
} }
switch obj.Spec.Verify.Provider { switch obj.Spec.Verify.Provider {

View File

@ -922,12 +922,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) {
} }
}, },
want: sreconcile.ResultEmpty, want: sreconcile.ResultEmpty,
wantErr: &serror.Event{Err: errors.New("failed to get secret 'invalid'")}, wantErr: &serror.Event{Err: errors.New("failed to get authentication secret '/invalid'")},
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) { assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
g.Expect(build.Complete()).To(BeFalse()) g.Expect(build.Complete()).To(BeFalse())
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret 'invalid'"), *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
})) }))
}, },
}, },
@ -1190,12 +1190,12 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
} }
}, },
want: sreconcile.ResultEmpty, want: sreconcile.ResultEmpty,
wantErr: &serror.Event{Err: errors.New("failed to get secret 'invalid'")}, wantErr: &serror.Event{Err: errors.New("failed to get authentication secret '/invalid'")},
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) { assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
g.Expect(build.Complete()).To(BeFalse()) g.Expect(build.Complete()).To(BeFalse())
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret 'invalid'"), *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
})) }))
}, },
}, },
@ -1649,83 +1649,6 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) {
} }
} }
func TestHelmChartReconciler_getHelmRepositorySecret(t *testing.T) {
mock := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "foo",
},
Data: map[string][]byte{
"key": []byte("bar"),
},
}
r := &HelmChartReconciler{
Client: fakeclient.NewClientBuilder().
WithObjects(mock).
Build(),
patchOptions: getPatchOptions(helmChartReadyCondition.Owned, "sc"),
}
tests := []struct {
name string
repository *helmv1.HelmRepository
want *corev1.Secret
wantErr bool
}{
{
name: "Existing secret reference",
repository: &helmv1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: mock.Namespace,
},
Spec: helmv1.HelmRepositorySpec{
SecretRef: &meta.LocalObjectReference{
Name: mock.Name,
},
},
},
want: mock,
},
{
name: "Empty secret reference",
repository: &helmv1.HelmRepository{
Spec: helmv1.HelmRepositorySpec{
SecretRef: nil,
},
},
want: nil,
},
{
name: "Error on client error",
repository: &helmv1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: "different",
},
Spec: helmv1.HelmRepositorySpec{
SecretRef: &meta.LocalObjectReference{
Name: mock.Name,
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got, err := r.getHelmRepositorySecret(context.TODO(), tt.repository)
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
})
}
}
func TestHelmChartReconciler_getSource(t *testing.T) { func TestHelmChartReconciler_getSource(t *testing.T) {
mocks := []client.Object{ mocks := []client.Object{
&helmv1.HelmRepository{ &helmv1.HelmRepository{

View File

@ -18,7 +18,6 @@ package controller
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@ -29,7 +28,6 @@ import (
helmgetter "helm.sh/helm/v3/pkg/getter" helmgetter "helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record" kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@ -390,59 +388,33 @@ func (r *HelmRepositoryReconciler) reconcileStorage(ctx context.Context, sp *pat
// pointer is set to the newly fetched index. // pointer is set to the newly fetched index.
func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher, func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher,
obj *helmv1.HelmRepository, artifact *sourcev1.Artifact, chartRepo *repository.ChartRepository) (sreconcile.Result, error) { obj *helmv1.HelmRepository, artifact *sourcev1.Artifact, chartRepo *repository.ChartRepository) (sreconcile.Result, error) {
var tlsConfig *tls.Config normalizedURL, err := repository.NormalizeURL(obj.Spec.URL)
if err != nil {
// Configure Helm client to access repository e := &serror.Stalling{
clientOpts := []helmgetter.Option{ Err: fmt.Errorf("invalid Helm repository URL: %w", err),
helmgetter.WithTimeout(obj.Spec.Timeout.Duration), Reason: sourcev1.URLInvalidReason,
helmgetter.WithURL(obj.Spec.URL),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
}
// Configure any authentication related options
if obj.Spec.SecretRef != nil {
// Attempt to retrieve secret
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 := &serror.Event{
Err: fmt.Errorf("failed to get secret '%s': %w", name.String(), err),
Reason: sourcev1.AuthenticationFailedReason,
} }
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
// Construct actual options clientOpts, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
opts, err := getter.ClientOptionsFromSecret(secret)
if err != nil { if err != nil {
if errors.Is(err, getter.ErrDeprecatedTLSConfig) {
ctrl.LoggerFrom(ctx).
Info("warning: specifying TLS authentication data via `.spec.secretRef` is deprecated, please use `.spec.certSecretRef` instead")
} else {
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())
// Return err as the content of the secret may change.
return sreconcile.ResultEmpty, e
}
clientOpts = append(clientOpts, opts...)
tlsConfig, err = getter.TLSClientConfigFromSecret(secret, obj.Spec.URL)
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 return sreconcile.ResultEmpty, e
} }
} }
// Construct Helm chart repository with options and download index // Construct Helm chart repository with options and download index
newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, tlsConfig, clientOpts...) newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, clientOpts.TlsConfig, clientOpts.GetterOpts...)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *url.Error: case *url.Error:

View File

@ -54,6 +54,7 @@ import (
"github.com/fluxcd/source-controller/internal/helm/registry" "github.com/fluxcd/source-controller/internal/helm/registry"
"github.com/fluxcd/source-controller/internal/helm/repository" "github.com/fluxcd/source-controller/internal/helm/repository"
"github.com/fluxcd/source-controller/internal/object" "github.com/fluxcd/source-controller/internal/object"
soci "github.com/fluxcd/source-controller/internal/oci"
intpredicates "github.com/fluxcd/source-controller/internal/predicates" intpredicates "github.com/fluxcd/source-controller/internal/predicates"
) )
@ -318,7 +319,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
return return
} }
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI { } else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider) auth, authErr := soci.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())

View File

@ -388,7 +388,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
assertConditions []metav1.Condition assertConditions []metav1.Condition
}{ }{
{ {
name: "HTTPS with secretRef pointing to CA cert but public repo URL succeeds", name: "HTTPS with certSecretRef pointing to CA cert but public repo URL succeeds",
protocol: "http", protocol: "http",
url: "https://stefanprodan.github.io/podinfo", url: "https://stefanprodan.github.io/podinfo",
want: sreconcile.ResultSuccess, want: sreconcile.ResultSuccess,
@ -400,6 +400,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
"caFile": tlsCA, "caFile": tlsCA,
}, },
}, },
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
assertConditions: []metav1.Condition{ assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"), *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"), *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -450,37 +453,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
}, },
}, },
{ {
name: "HTTPS with CAFile secret makes ArtifactOutdated=True", name: "HTTPS with invalid CAFile in certSecretRef makes FetchFailed=True and returns error",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
},
Data: map[string][]byte{
"caFile": tlsCA,
},
},
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
},
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
t.Expect(chartRepo.Path).ToNot(BeEmpty())
t.Expect(chartRepo.Index).ToNot(BeNil())
t.Expect(artifact.Revision).ToNot(BeEmpty())
},
},
{
name: "HTTPS with invalid CAFile secret makes FetchFailed=True and returns error",
protocol: "https", protocol: "https",
server: options{ server: options{
publicKey: tlsPublicKey, publicKey: tlsPublicKey,
@ -496,13 +469,13 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
}, },
}, },
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) { beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-ca"} obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo") conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar") conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
}, },
wantErr: true, wantErr: true,
assertConditions: []metav1.Condition{ assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to create TLS client config with secret data: cannot append certificate into certificate pool: invalid caFile"), *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid caFile"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"), *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"), *conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
}, },
@ -766,32 +739,32 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
} }
// Calculate the artifact digest for valid repos configurations. // Calculate the artifact digest for valid repos configurations.
clientOpts := []helmgetter.Option{ getterOpts := []helmgetter.Option{
helmgetter.WithURL(server.URL()), helmgetter.WithURL(server.URL()),
} }
var newChartRepo *repository.ChartRepository var newChartRepo *repository.ChartRepository
var tOpts *tls.Config var tlsConf *tls.Config
validSecret := true validSecret := true
if secret != nil { if secret != nil {
// Extract the client options from secret, ignoring any invalid // Extract the client options from secret, ignoring any invalid
// value. validSecret is used to determine if the index digest // value. validSecret is used to determine if the index digest
// should be calculated below. // should be calculated below.
var cOpts []helmgetter.Option var gOpts []helmgetter.Option
var serr error var serr error
cOpts, serr = getter.ClientOptionsFromSecret(*secret) gOpts, serr = getter.GetterOptionsFromSecret(*secret)
if serr != nil { if serr != nil {
validSecret = false validSecret = false
} }
clientOpts = append(clientOpts, cOpts...) getterOpts = append(getterOpts, gOpts...)
repoURL := server.URL() repoURL := server.URL()
if tt.url != "" { if tt.url != "" {
repoURL = tt.url repoURL = tt.url
} }
tOpts, serr = getter.TLSClientConfigFromSecret(*secret, repoURL) tlsConf, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
if serr != nil { if serr != nil {
validSecret = false validSecret = false
} }
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts...) newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
} else { } else {
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil) newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
} }
@ -807,9 +780,6 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
g.Expect(newChartRepo.LoadFromPath()).To(Succeed()) g.Expect(newChartRepo.LoadFromPath()).To(Succeed())
rev = newChartRepo.Digest(intdigest.Canonical) rev = newChartRepo.Digest(intdigest.Canonical)
} }
if tt.beforeFunc != nil {
tt.beforeFunc(g, obj, rev, dig)
}
r := &HelmRepositoryReconciler{ r := &HelmRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
@ -818,6 +788,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
Getters: testGetters, Getters: testGetters,
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"), patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
} }
if tt.beforeFunc != nil {
tt.beforeFunc(g, obj, rev, dig)
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred()) g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
defer func() { defer func() {

View File

@ -55,7 +55,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/oci" "github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/conditions"
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"
@ -345,7 +344,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != ociv1.GenericOCIProvider && ok { if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != ociv1.GenericOCIProvider && ok {
var authErr error var authErr error
auth, authErr = oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider) auth, authErr = soci.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 := serror.NewGeneric( e := serror.NewGeneric(
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr), fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
@ -870,27 +869,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *ociv1.OCIR
return transport, nil return transport, nil
} }
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
u := strings.TrimPrefix(url, ociv1.OCIRepositoryPrefix)
ref, err := name.ParseReference(u)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
}
opts := login.ProviderOptions{}
switch provider {
case ociv1.AmazonOCIProvider:
opts.AwsAutoLogin = true
case ociv1.AzureOCIProvider:
opts.AzureAutoLogin = true
case ociv1.GoogleOCIProvider:
opts.GcpAutoLogin = true
}
return login.NewManager().Login(ctx, u, ref, opts)
}
// reconcileStorage ensures the current state of the storage matches the // reconcileStorage ensures the current state of the storage matches the
// desired and previously observed state. // desired and previously observed state.
// //

View File

@ -0,0 +1,196 @@
/*
Copyright 2023 The Flux 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 getter
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/url"
"github.com/fluxcd/pkg/oci"
"github.com/google/go-containerregistry/pkg/authn"
helmgetter "helm.sh/helm/v3/pkg/getter"
helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/source-controller/internal/helm/registry"
soci "github.com/fluxcd/source-controller/internal/oci"
)
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
// ClientOpts contains the various options to use while constructing
// a Helm repository client.
type ClientOpts struct {
Authenticator authn.Authenticator
Keychain authn.Keychain
RegLoginOpt helmreg.LoginOption
TlsConfig *tls.Config
GetterOpts []helmgetter.Option
}
// GetClientOpts uses the provided HelmRepository object and a normalized
// URL to construct a HelmClientOpts object. If obj is an OCI HelmRepository,
// then the returned options object will also contain the required registry
// auth mechanisms.
func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmRepository, url string) (*ClientOpts, error) {
hrOpts := &ClientOpts{
GetterOpts: []helmgetter.Option{
helmgetter.WithURL(url),
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
},
}
ociRepo := obj.Spec.Type == helmv1.HelmRepositoryTypeOCI
var certSecret *corev1.Secret
var err error
// Check `.spec.certSecretRef` first for any TLS auth data.
if obj.Spec.CertSecretRef != nil {
certSecret, err = fetchSecret(ctx, c, obj.Spec.CertSecretRef.Name, obj.GetNamespace())
if err != nil {
return nil, fmt.Errorf("failed to get TLS authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.CertSecretRef.Name, err)
}
hrOpts.TlsConfig, err = TLSClientConfigFromSecret(*certSecret, url)
if err != nil {
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
}
}
var authSecret *corev1.Secret
var deprecatedTLSConfig bool
if obj.Spec.SecretRef != nil {
authSecret, err = fetchSecret(ctx, c, obj.Spec.SecretRef.Name, obj.GetNamespace())
if err != nil {
return nil, fmt.Errorf("failed to get authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.SecretRef.Name, err)
}
// Construct actual Helm client options.
opts, err := GetterOptionsFromSecret(*authSecret)
if err != nil {
return nil, fmt.Errorf("failed to configure Helm client: %w", err)
}
hrOpts.GetterOpts = append(hrOpts.GetterOpts, opts...)
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
// then try to use `.spec.certSecretRef`.
if hrOpts.TlsConfig == nil {
hrOpts.TlsConfig, err = TLSClientConfigFromSecret(*authSecret, url)
if err != nil {
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
}
// Constructing a TLS config using the auth secret is deprecated behavior.
if hrOpts.TlsConfig != nil {
deprecatedTLSConfig = true
}
}
if ociRepo {
hrOpts.Keychain, err = registry.LoginOptionFromSecret(url, *authSecret)
if err != nil {
return nil, fmt.Errorf("failed to configure login options: %w", err)
}
}
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI && ociRepo {
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
return nil, fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
}
if authenticator != nil {
hrOpts.Authenticator = authenticator
}
}
if ociRepo {
hrOpts.RegLoginOpt, err = registry.NewLoginOption(hrOpts.Authenticator, hrOpts.Keychain, url)
if err != nil {
return nil, err
}
}
if deprecatedTLSConfig {
err = ErrDeprecatedTLSConfig
}
return hrOpts, err
}
func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
key := types.NamespacedName{
Namespace: namespace,
Name: name,
}
var secret corev1.Secret
if err := c.Get(ctx, key, &secret); err != nil {
return nil, err
}
return &secret, nil
}
// TLSClientConfigFromSecret attempts to construct a TLS client config
// for the given v1.Secret. It returns the TLS client config or an error.
//
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
// certBytes OR keyBytes is defined it returns an error.
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, error) {
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
switch {
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
return nil, nil
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
return nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
secret.Name)
}
tlsConf := &tls.Config{}
if len(certBytes) > 0 && len(keyBytes) > 0 {
cert, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
return nil, err
}
tlsConf.Certificates = append(tlsConf.Certificates, cert)
}
if len(caBytes) > 0 {
cp, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
}
if !cp.AppendCertsFromPEM(caBytes) {
return nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
}
tlsConf.RootCAs = cp
}
tlsConf.BuildNameToCertificate()
u, err := url.Parse(repositoryUrl)
if err != nil {
return nil, fmt.Errorf("cannot parse repository URL: %w", err)
}
tlsConf.ServerName = u.Hostname()
return tlsConf, nil
}

View File

@ -0,0 +1,254 @@
/*
Copyright 2023 The Flux 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 getter
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"math/big"
"os"
"testing"
"time"
"github.com/fluxcd/pkg/apis/meta"
"github.com/google/go-containerregistry/pkg/name"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
)
func TestGetClientOpts(t *testing.T) {
tlsCA, err := os.ReadFile("../../controller/testdata/certs/ca.pem")
if err != nil {
t.Errorf("could not read CA file: %s", err)
}
tests := []struct {
name string
certSecret *corev1.Secret
authSecret *corev1.Secret
afterFunc func(t *WithT, hcOpts *ClientOpts)
oci bool
err error
}{
{
name: "HelmRepository with certSecretRef discards TLS config in secretRef",
certSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
},
Data: map[string][]byte{
"caFile": tlsCA,
},
},
authSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "auth",
},
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
"caFile": []byte("invalid"),
},
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
},
},
{
name: "HelmRepository with TLS config only in secretRef is marked as deprecated",
authSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "auth-tls",
},
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
"caFile": tlsCA,
},
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
},
err: ErrDeprecatedTLSConfig,
},
{
name: "OCI HelmRepository with secretRef has auth configured",
authSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "auth-oci",
},
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
},
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
repo, err := name.NewRepository("ghcr.io/dummy")
t.Expect(err).ToNot(HaveOccurred())
authenticator, err := hcOpts.Keychain.Resolve(repo)
t.Expect(err).ToNot(HaveOccurred())
config, err := authenticator.Authorization()
t.Expect(err).ToNot(HaveOccurred())
t.Expect(config.Username).To(Equal("user"))
t.Expect(config.Password).To(Equal("pass"))
},
oci: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
helmRepo := &helmv1.HelmRepository{
Spec: helmv1.HelmRepositorySpec{
Timeout: &metav1.Duration{
Duration: time.Second,
},
},
}
if tt.oci {
helmRepo.Spec.Type = helmv1.HelmRepositoryTypeOCI
}
clientBuilder := fakeclient.NewClientBuilder()
if tt.authSecret != nil {
clientBuilder.WithObjects(tt.authSecret.DeepCopy())
helmRepo.Spec.SecretRef = &meta.LocalObjectReference{
Name: tt.authSecret.Name,
}
}
if tt.certSecret != nil {
clientBuilder.WithObjects(tt.certSecret.DeepCopy())
helmRepo.Spec.CertSecretRef = &meta.LocalObjectReference{
Name: tt.certSecret.Name,
}
}
c := clientBuilder.Build()
clientOpts, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
if tt.err != nil {
g.Expect(err).To(Equal(tt.err))
} else {
g.Expect(err).ToNot(HaveOccurred())
}
tt.afterFunc(g, clientOpts)
})
}
}
func Test_tlsClientConfigFromSecret(t *testing.T) {
tlsSecretFixture := validTlsSecret(t)
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
wantNil bool
}{
{"certFile, keyFile and caFile", tlsSecretFixture, nil, false, false},
{"without certFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "certFile") }, true, true},
{"without keyFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "keyFile") }, true, true},
{"without caFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "caFile") }, false, false},
{"empty", corev1.Secret{}, nil, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
got, err := TLSClientConfigFromSecret(*secret, "")
if (err != nil) != tt.wantErr {
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantNil && got != nil {
t.Error("TLSClientConfigFromSecret() != nil")
return
}
})
}
}
// validTlsSecret creates a secret containing key pair and CA certificate that are
// valid from a syntax (minimum requirements) perspective.
func validTlsSecret(t *testing.T) corev1.Secret {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal("Private key cannot be created.", err.Error())
}
certTemplate := x509.Certificate{
SerialNumber: big.NewInt(1337),
}
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
if err != nil {
t.Fatal("Certificate cannot be created.", err.Error())
}
ca := &x509.Certificate{
SerialNumber: big.NewInt(7331),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Fatal("CA private key cannot be created.", err.Error())
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
t.Fatal("CA certificate cannot be created.", err.Error())
}
keyPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
certPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
})
caPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
return corev1.Secret{
Data: map[string][]byte{
"certFile": []byte(certPem),
"keyFile": []byte(keyPem),
"caFile": []byte(caPem),
},
}
}

View File

@ -17,20 +17,17 @@ limitations under the License.
package getter package getter
import ( import (
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net/url"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
) )
// ClientOptionsFromSecret constructs a getter.Option slice for the given secret. // GetterOptionsFromSecret constructs a getter.Option slice for the given secret.
// It returns the slice, or an error. // It returns the slice, or an error.
func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) { func GetterOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
var opts []getter.Option var opts []getter.Option
basicAuth, err := BasicAuthFromSecret(secret) basicAuth, err := basicAuthFromSecret(secret)
if err != nil { if err != nil {
return opts, err return opts, err
} }
@ -40,12 +37,12 @@ func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
return opts, nil return opts, nil
} }
// BasicAuthFromSecret attempts to construct a basic auth getter.Option for the // basicAuthFromSecret attempts to construct a basic auth getter.Option for the
// given v1.Secret and returns the result. // given v1.Secret and returns the result.
// //
// Secrets with no username AND password are ignored, if only one is defined it // Secrets with no username AND password are ignored, if only one is defined it
// returns an error. // returns an error.
func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) { func basicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
username, password := string(secret.Data["username"]), string(secret.Data["password"]) username, password := string(secret.Data["username"]), string(secret.Data["password"])
switch { switch {
case username == "" && password == "": case username == "" && password == "":
@ -55,51 +52,3 @@ func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
} }
return getter.WithBasicAuth(username, password), nil return getter.WithBasicAuth(username, password), nil
} }
// TLSClientConfigFromSecret attempts to construct a TLS client config
// for the given v1.Secret. It returns the TLS client config or an error.
//
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
// certBytes OR keyBytes is defined it returns an error.
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, error) {
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
switch {
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
return nil, nil
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
return nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
secret.Name)
}
tlsConf := &tls.Config{}
if len(certBytes) > 0 && len(keyBytes) > 0 {
cert, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
return nil, err
}
tlsConf.Certificates = append(tlsConf.Certificates, cert)
}
if len(caBytes) > 0 {
cp, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
}
if !cp.AppendCertsFromPEM(caBytes) {
return nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
}
tlsConf.RootCAs = cp
}
tlsConf.BuildNameToCertificate()
u, err := url.Parse(repositoryUrl)
if err != nil {
return nil, fmt.Errorf("cannot parse repository URL: %w", err)
}
tlsConf.ServerName = u.Hostname()
return tlsConf, nil
}

View File

@ -17,11 +17,6 @@ limitations under the License.
package getter package getter
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"math/big"
"testing" "testing"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -36,7 +31,7 @@ var (
} }
) )
func TestClientOptionsFromSecret(t *testing.T) { func TestGetterOptionsFromSecret(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
secrets []corev1.Secret secrets []corev1.Secret
@ -53,7 +48,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
} }
} }
got, err := ClientOptionsFromSecret(secret) got, err := GetterOptionsFromSecret(secret)
if err != nil { if err != nil {
t.Errorf("ClientOptionsFromSecret() error = %v", err) t.Errorf("ClientOptionsFromSecret() error = %v", err)
return return
@ -65,7 +60,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
} }
} }
func TestBasicAuthFromSecret(t *testing.T) { func Test_basicAuthFromSecret(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
secret corev1.Secret secret corev1.Secret
@ -84,7 +79,7 @@ func TestBasicAuthFromSecret(t *testing.T) {
if tt.modify != nil { if tt.modify != nil {
tt.modify(secret) tt.modify(secret)
} }
got, err := BasicAuthFromSecret(*secret) got, err := basicAuthFromSecret(*secret)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr)
return return
@ -96,96 +91,3 @@ func TestBasicAuthFromSecret(t *testing.T) {
}) })
} }
} }
func TestTLSClientConfigFromSecret(t *testing.T) {
tlsSecretFixture := validTlsSecret(t)
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
wantNil bool
}{
{"certFile, keyFile and caFile", tlsSecretFixture, nil, false, false},
{"without certFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "certFile") }, true, true},
{"without keyFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "keyFile") }, true, true},
{"without caFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "caFile") }, false, false},
{"empty", corev1.Secret{}, nil, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
got, err := TLSClientConfigFromSecret(*secret, "")
if (err != nil) != tt.wantErr {
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantNil && got != nil {
t.Error("TLSClientConfigFromSecret() != nil")
return
}
})
}
}
// validTlsSecret creates a secret containing key pair and CA certificate that are
// valid from a syntax (minimum requirements) perspective.
func validTlsSecret(t *testing.T) corev1.Secret {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal("Private key cannot be created.", err.Error())
}
certTemplate := x509.Certificate{
SerialNumber: big.NewInt(1337),
}
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
if err != nil {
t.Fatal("Certificate cannot be created.", err.Error())
}
ca := &x509.Certificate{
SerialNumber: big.NewInt(7331),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Fatal("CA private key cannot be created.", err.Error())
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
t.Fatal("CA certificate cannot be created.", err.Error())
}
keyPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
certPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
})
caPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
return corev1.Secret{
Data: map[string][]byte{
"certFile": []byte(certPem),
"keyFile": []byte(keyPem),
"caFile": []byte(caPem),
},
}
}

View File

@ -26,6 +26,7 @@ import (
"github.com/fluxcd/source-controller/internal/oci" "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"
helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
) )
@ -139,3 +140,17 @@ func (r stringResource) String() string {
func (r stringResource) RegistryStr() string { func (r stringResource) RegistryStr() string {
return r.registry return r.registry
} }
// NewLoginOption returns a registry login option for the given HelmRepository.
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
func NewLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (helmreg.LoginOption, error) {
if auth != nil {
return AuthAdaptHelper(auth)
}
if keychain != nil {
return KeychainAdaptHelper(keychain)(registryURL)
}
return nil, nil
}

View File

@ -16,7 +16,17 @@ limitations under the License.
package oci package oci
import "github.com/google/go-containerregistry/pkg/authn" import (
"context"
"fmt"
"strings"
"github.com/fluxcd/pkg/oci/auth/login"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
// Anonymous is an authn.AuthConfig that always returns an anonymous // Anonymous is an authn.AuthConfig that always returns an anonymous
// authenticator. It is useful for registries that do not require authentication // authenticator. It is useful for registries that do not require authentication
@ -28,3 +38,24 @@ type Anonymous authn.AuthConfig
func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) { func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) {
return authn.Anonymous, nil return authn.Anonymous, nil
} }
// OIDCAuth generates the OIDC credential authenticator based on the specified cloud provider.
func OIDCAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
u := strings.TrimPrefix(url, sourcev1.OCIRepositoryPrefix)
ref, err := name.ParseReference(u)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
}
opts := login.ProviderOptions{}
switch provider {
case sourcev1.AmazonOCIProvider:
opts.AwsAutoLogin = true
case sourcev1.AzureOCIProvider:
opts.AzureAutoLogin = true
case sourcev1.GoogleOCIProvider:
opts.GcpAutoLogin = true
}
return login.NewManager().Login(ctx, u, ref, opts)
}