helm: add support for specifying TLS auth via `.spec.certSecretRef`

Add support for specifying TLS auth data via `.spec.certSecretRef` in
HelmRepository and log a deprecation warning if TLS is configured via
`.spec.secretRef`. Introduce (and refactor) Helm client builder and
auth helpers to reduce duplicated code and increase uniformity and
testability.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
Sanskar Jaiswal 2023-07-18 18:38:20 +05:30
parent 9986d9918e
commit 79adec586b
No known key found for this signature in database
GPG Key ID: 5982D0279C227FFD
12 changed files with 574 additions and 513 deletions

View File

@ -18,7 +18,6 @@ package controller
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/url"
@ -28,7 +27,6 @@ import (
"strings"
"time"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/opencontainers/go-digest"
helmgetter "helm.sh/helm/v3/pkg/getter"
@ -54,7 +52,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/patch"
@ -68,7 +65,6 @@ import (
serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/registry"
"github.com/fluxcd/source-controller/internal/helm/repository"
soci "github.com/fluxcd/source-controller/internal/oci"
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.
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *helmv1.HelmChart,
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
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
defer cancel()
@ -519,65 +510,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
if err != nil {
return chartRepoConfigErrorReturn(err, obj)
}
// Construct the Getter options from the HelmRepository data
clientOpts := []helmgetter.Option{
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 {
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
e := &serror.Event{
Err: err,
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())
return sreconcile.ResultEmpty, e
}
getterOpts := clientOpts.GetterOpts
// Initialize the chart repository
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.
// TODO@souleb: remove this once the registry move to Oras v2
// 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 {
e := &serror.Event{
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
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
verifiers, err = r.makeVerifiers(ctx, obj, authenticator, keychain)
verifiers, err = r.makeVerifiers(ctx, obj, *clientOpts)
if err != nil {
if obj.Spec.Verify.SecretRef == nil {
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
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
repository.WithOCIGetter(r.Getters),
repository.WithOCIGetterOptions(clientOpts),
repository.WithOCIGetterOptions(getterOpts),
repository.WithOCIRegistryClient(registryClient),
repository.WithVerifiers(verifiers))
if err != nil {
return chartRepoConfigErrorReturn(err, obj)
}
chartRepo = ociChartRepo
// If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart
if loginOpt != nil {
err = ociChartRepo.Login(loginOpt)
if clientOpts.RegLoginOpt != nil {
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
if err != nil {
e := &serror.Event{
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
}
}
chartRepo = ociChartRepo
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 {
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.
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
return func(url string) (repository.Downloader, error) {
var (
tlsConfig *tls.Config
authenticator authn.Authenticator
keychain authn.Keychain
)
normalizedURL, err := repository.NormalizeURL(url)
if err != nil {
return nil, err
@ -1052,61 +981,28 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel()
clientOpts := []helmgetter.Option{
helmgetter.WithURL(normalizedURL),
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 {
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
return nil, err
}
getterOpts := clientOpts.GetterOpts
var chartRepo repository.Downloader
if helmreg.IsOCI(normalizedURL) {
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != 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
// 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),
repository.WithOCIGetterOptions(clientOpts),
repository.WithOCIGetterOptions(getterOpts),
repository.WithOCIRegistryClient(registryClient),
repository.WithCredentialsFile(credentialsFile))
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
if credentialsFile != "" {
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
// The OCIGetter will later retrieve the stored credentials to pull the chart
if loginOpt != nil {
err = ociChartRepo.Login(loginOpt)
if clientOpts.RegLoginOpt != nil {
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
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
errs = append(errs, ociChartRepo.Clear())
return nil, kerrors.NewAggregate(errs)
@ -1130,7 +1026,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
chartRepo = ociChartRepo
} else {
httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, tlsConfig, clientOpts...)
httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, clientOpts.TlsConfig, getterOpts...)
if err != nil {
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)
}
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 {
repo, ok := o.(*helmv1.HelmRepository)
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.
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
verifyOpts := []remote.Option{}
if auth != nil {
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
if clientOpts.Authenticator != nil {
verifyOpts = append(verifyOpts, remote.WithAuth(clientOpts.Authenticator))
} else {
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(clientOpts.Keychain))
}
switch obj.Spec.Verify.Provider {

View File

@ -922,12 +922,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) {
}
},
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) {
g.Expect(build.Complete()).To(BeFalse())
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,
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) {
g.Expect(build.Complete()).To(BeFalse())
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) {
mocks := []client.Object{
&helmv1.HelmRepository{

View File

@ -18,7 +18,6 @@ package controller
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/url"
@ -29,7 +28,6 @@ import (
helmgetter "helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"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.
func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher,
obj *helmv1.HelmRepository, artifact *sourcev1.Artifact, chartRepo *repository.ChartRepository) (sreconcile.Result, error) {
var tlsConfig *tls.Config
// Configure Helm client to access repository
clientOpts := []helmgetter.Option{
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
helmgetter.WithURL(obj.Spec.URL),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
normalizedURL, err := repository.NormalizeURL(obj.Spec.URL)
if err != nil {
e := &serror.Stalling{
Err: fmt.Errorf("invalid Helm repository URL: %w", err),
Reason: sourcev1.URLInvalidReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
// 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 {
clientOpts, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
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{
Err: fmt.Errorf("failed to get secret '%s': %w", name.String(), err),
Err: err,
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
// Construct actual options
opts, err := getter.ClientOptionsFromSecret(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())
// 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
}
}
// 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 {
switch err.(type) {
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/repository"
"github.com/fluxcd/source-controller/internal/object"
soci "github.com/fluxcd/source-controller/internal/oci"
intpredicates "github.com/fluxcd/source-controller/internal/predicates"
)
@ -318,7 +319,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
return
}
} 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) {
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())

View File

@ -388,7 +388,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
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",
url: "https://stefanprodan.github.io/podinfo",
want: sreconcile.ResultSuccess,
@ -400,6 +400,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
"caFile": tlsCA,
},
},
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
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"),
@ -450,37 +453,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
},
{
name: "HTTPS with CAFile secret makes ArtifactOutdated=True",
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",
name: "HTTPS with invalid CAFile in certSecretRef makes FetchFailed=True and returns error",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
@ -496,13 +469,13 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
},
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.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
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.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
@ -766,32 +739,32 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
}
// Calculate the artifact digest for valid repos configurations.
clientOpts := []helmgetter.Option{
getterOpts := []helmgetter.Option{
helmgetter.WithURL(server.URL()),
}
var newChartRepo *repository.ChartRepository
var tOpts *tls.Config
var tlsConf *tls.Config
validSecret := true
if secret != nil {
// Extract the client options from secret, ignoring any invalid
// value. validSecret is used to determine if the index digest
// should be calculated below.
var cOpts []helmgetter.Option
var gOpts []helmgetter.Option
var serr error
cOpts, serr = getter.ClientOptionsFromSecret(*secret)
gOpts, serr = getter.GetterOptionsFromSecret(*secret)
if serr != nil {
validSecret = false
}
clientOpts = append(clientOpts, cOpts...)
getterOpts = append(getterOpts, gOpts...)
repoURL := server.URL()
if tt.url != "" {
repoURL = tt.url
}
tOpts, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
tlsConf, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
if serr != nil {
validSecret = false
}
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts...)
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
} else {
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())
rev = newChartRepo.Digest(intdigest.Canonical)
}
if tt.beforeFunc != nil {
tt.beforeFunc(g, obj, rev, dig)
}
r := &HelmRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32),
@ -818,6 +788,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
Getters: testGetters,
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())
defer func() {

View File

@ -55,7 +55,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"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 {
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) {
e := serror.NewGeneric(
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
}
// 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
// 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
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"
"helm.sh/helm/v3/pkg/getter"
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.
func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
func GetterOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
var opts []getter.Option
basicAuth, err := BasicAuthFromSecret(secret)
basicAuth, err := basicAuthFromSecret(secret)
if err != nil {
return opts, err
}
@ -40,12 +37,12 @@ func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
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.
//
// Secrets with no username AND password are ignored, if only one is defined it
// 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"])
switch {
case username == "" && password == "":
@ -55,51 +52,3 @@ func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
}
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
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"math/big"
"testing"
corev1 "k8s.io/api/core/v1"
@ -36,7 +31,7 @@ var (
}
)
func TestClientOptionsFromSecret(t *testing.T) {
func TestGetterOptionsFromSecret(t *testing.T) {
tests := []struct {
name string
secrets []corev1.Secret
@ -53,7 +48,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
}
}
got, err := ClientOptionsFromSecret(secret)
got, err := GetterOptionsFromSecret(secret)
if err != nil {
t.Errorf("ClientOptionsFromSecret() error = %v", err)
return
@ -65,7 +60,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
}
}
func TestBasicAuthFromSecret(t *testing.T) {
func Test_basicAuthFromSecret(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
@ -84,7 +79,7 @@ func TestBasicAuthFromSecret(t *testing.T) {
if tt.modify != nil {
tt.modify(secret)
}
got, err := BasicAuthFromSecret(*secret)
got, err := basicAuthFromSecret(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr)
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/google/go-containerregistry/pkg/authn"
"helm.sh/helm/v3/pkg/registry"
helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1"
)
@ -139,3 +140,17 @@ func (r stringResource) String() string {
func (r stringResource) RegistryStr() string {
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
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
// 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) {
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)
}