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:
parent
9986d9918e
commit
79adec586b
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue