From 869c73d0ad6a2321b0a69205c0e3c139183bd376 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Thu, 1 Sep 2022 13:37:41 +0200 Subject: [PATCH] secretRef take precedence over provider if secretRef is provided, we do not attempt to resolve oidc Signed-off-by: Soule BA --- controllers/helmchart_controller.go | 12 +- controllers/helmchart_controller_test.go | 230 +++++++++++++++++- controllers/helmrepository_controller_oci.go | 44 +--- .../helmrepository_controller_oci_test.go | 149 ++++++++++++ controllers/ocirepository_controller.go | 20 +- controllers/ocirepository_controller_test.go | 42 ++++ controllers/suite_test.go | 11 +- internal/util/auth.go | 30 +++ 8 files changed, 468 insertions(+), 70 deletions(-) create mode 100644 internal/util/auth.go diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 24650f5e0..965ddcedc 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -516,10 +516,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * } loginOpts = append([]helmreg.LoginOption{}, loginOpt) - } - - if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { - auth, authErr := oidcAuth(ctxTimeout, repo) + } else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + auth, authErr := oidcAuthFromAdapter(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), @@ -991,10 +989,8 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont } loginOpts = append([]helmreg.LoginOption{}, loginOpt) - } - - if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { - auth, authErr := oidcAuth(ctxTimeout, repo) + } else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider) if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr) } diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index e9c3920d2..631286bc1 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -44,6 +44,7 @@ import ( kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/fluxcd/pkg/apis/meta" @@ -893,21 +894,11 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) { chartPath = "testdata/charts/helmchart-0.1.0.tgz" ) - // Login to the registry - err := testRegistryServer.registryClient.Login(testRegistryServer.registryHost, - helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword), - helmreg.LoginOptInsecure(true)) - g.Expect(err).NotTo(HaveOccurred()) - // Load a test chart chartData, err := ioutil.ReadFile(chartPath) - g.Expect(err).NotTo(HaveOccurred()) - metadata, err := extractChartMeta(chartData) - g.Expect(err).NotTo(HaveOccurred()) // Upload the test chart - ref := fmt.Sprintf("%s/testrepo/%s:%s", testRegistryServer.registryHost, metadata.Name, metadata.Version) - _, err = testRegistryServer.registryClient.Push(chartData, ref) + metadata, err := loadTestChartToOCI(chartData, chartPath, testRegistryServer) g.Expect(err).NotTo(HaveOccurred()) storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords) @@ -2038,6 +2029,194 @@ func TestHelmChartReconciler_notify(t *testing.T) { } } +func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) { + const ( + chartPath = "testdata/charts/helmchart-0.1.0.tgz" + ) + + type secretOptions struct { + username string + password string + } + + tests := []struct { + name string + url string + registryOpts registryOptions + secretOpts secretOptions + provider string + providerImg string + want sreconcile.Result + wantErr bool + assertConditions []metav1.Condition + }{ + { + name: "HTTP without basic auth", + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '' chart with version ''"), + }, + }, + { + name: "HTTP with basic auth secret", + want: sreconcile.ResultSuccess, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: testRegistryUsername, + password: testRegistryPassword, + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '' chart with version ''"), + }, + }, + { + name: "HTTP registry - basic auth with invalid secret", + want: sreconcile.ResultEmpty, + wantErr: true, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: "wrong-pass", + password: "wrong-pass", + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to login to OCI registry"), + }, + }, + { + name: "with contextual login provider", + wantErr: true, + provider: "aws", + providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test", + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to get credential from"), + }, + }, + { + name: "with contextual login provider and secretRef", + want: sreconcile.ResultSuccess, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: testRegistryUsername, + password: testRegistryPassword, + }, + provider: "azure", + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '' chart with version ''"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme()) + workspaceDir := t.TempDir() + server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts) + + g.Expect(err).NotTo(HaveOccurred()) + + // Load a test chart + chartData, err := ioutil.ReadFile(chartPath) + + // Upload the test chart + metadata, err := loadTestChartToOCI(chartData, chartPath, server) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) + + repo := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "auth-strategy-", + }, + Spec: sourcev1.HelmRepositorySpec{ + Interval: metav1.Duration{Duration: interval}, + Timeout: &metav1.Duration{Duration: timeout}, + Type: sourcev1.HelmRepositoryTypeOCI, + Provider: sourcev1.GenericOCIProvider, + URL: fmt.Sprintf("oci://%s/testrepo", server.registryHost), + }, + } + + if tt.provider != "" { + repo.Spec.Provider = tt.provider + } + // If a provider specific image is provided, overwrite existing URL + // set earlier. It'll fail but it's necessary to set them because + // the login check expects the URLs to be of certain pattern. + if tt.providerImg != "" { + repo.Spec.URL = tt.providerImg + } + + if tt.secretOpts.username != "" && tt.secretOpts.password != "" { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "auth-secretref", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, + server.registryHost, tt.secretOpts.username, tt.secretOpts.password)), + }, + } + + repo.Spec.SecretRef = &meta.LocalObjectReference{ + Name: secret.Name, + } + builder.WithObjects(secret, repo) + } else { + builder.WithObjects(repo) + } + + obj := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "auth-strategy-", + }, + Spec: sourcev1.HelmChartSpec{ + Chart: metadata.Name, + Version: metadata.Version, + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: repo.Name, + }, + Interval: metav1.Duration{Duration: interval}, + }, + } + + r := &HelmChartReconciler{ + Client: builder.Build(), + EventRecorder: record.NewFakeRecorder(32), + Getters: testGetters, + RegistryClientGenerator: registry.ClientGenerator, + } + + var b chart.Build + defer func() { + if _, err := os.Stat(b.Path); !os.IsNotExist(err) { + err := os.Remove(b.Path) + g.Expect(err).NotTo(HaveOccurred()) + } + }() + + assertConditions := tt.assertConditions + for k := range assertConditions { + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", metadata.Name) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", metadata.Version) + } + + got, err := r.reconcileSource(ctx, obj, &b) + g.Expect(err != nil).To(Equal(tt.wantErr)) + g.Expect(got).To(Equal(tt.want)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions)) + }) + } +} + // extractChartMeta is used to extract a chart metadata from a byte array func extractChartMeta(chartData []byte) (*hchart.Metadata, error) { ch, err := loader.LoadArchive(bytes.NewReader(chartData)) @@ -2046,3 +2225,32 @@ func extractChartMeta(chartData []byte) (*hchart.Metadata, error) { } return ch.Metadata, nil } + +func loadTestChartToOCI(chartData []byte, chartPath string, server *registryClientTestServer) (*hchart.Metadata, error) { + // Login to the registry + err := server.registryClient.Login(server.registryHost, + helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword), + helmreg.LoginOptInsecure(true)) + if err != nil { + return nil, err + } + + // Load a test chart + chartData, err = ioutil.ReadFile(chartPath) + if err != nil { + return nil, err + } + metadata, err := extractChartMeta(chartData) + if err != nil { + return nil, err + } + + // Upload the test chart + ref := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version) + _, err = server.registryClient.Push(chartData, ref) + if err != nil { + return nil, err + } + + return metadata, nil +} diff --git a/controllers/helmrepository_controller_oci.go b/controllers/helmrepository_controller_oci.go index cb2df389c..02ec39b49 100644 --- a/controllers/helmrepository_controller_oci.go +++ b/controllers/helmrepository_controller_oci.go @@ -22,7 +22,6 @@ import ( "fmt" "net/url" "os" - "strings" "time" helmgetter "helm.sh/helm/v3/pkg/getter" @@ -42,12 +41,10 @@ import ( "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" "github.com/fluxcd/pkg/runtime/predicates" - "github.com/google/go-containerregistry/pkg/name" "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" @@ -294,10 +291,8 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta if loginOpt != nil { loginOpts = append(loginOpts, loginOpt) } - } - - if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { - auth, authErr := oidcAuth(ctxTimeout, obj) + } else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + auth, authErr := oidcAuthFromAdapter(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()) @@ -380,41 +375,12 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime r.Eventf(obj, eventType, reason, msg) } -// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider. -func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) { - url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) - ref, err := name.ParseReference(url) - if err != nil { - return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err) - } - - loginOpt, err := loginWithManager(ctx, obj.Spec.Provider, url, ref) - if err != nil { - return nil, fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err) - } - - return loginOpt, nil -} - -func loginWithManager(ctx context.Context, provider, url string, ref name.Reference) (helmreg.LoginOption, error) { - opts := login.ProviderOptions{} - switch provider { - case sourcev1.AmazonOCIProvider: - opts.AwsAutoLogin = true - case sourcev1.AzureOCIProvider: - opts.AzureAutoLogin = true - case sourcev1.GoogleOCIProvider: - opts.GcpAutoLogin = true - } - - auth, err := login.NewManager().Login(ctx, url, ref, opts) +// oidcAuthFromAdapter generates the OIDC credential authenticator based on the specified cloud provider. +func oidcAuthFromAdapter(ctx context.Context, url, provider string) (helmreg.LoginOption, error) { + auth, err := oidcAuth(ctx, url, provider) if err != nil { return nil, err } - if auth == nil { - return nil, nil - } - return registry.OIDCAdaptHelper(auth) } diff --git a/controllers/helmrepository_controller_oci_test.go b/controllers/helmrepository_controller_oci_test.go index ec75a67ef..c5e36c297 100644 --- a/controllers/helmrepository_controller_oci_test.go +++ b/controllers/helmrepository_controller_oci_test.go @@ -26,12 +26,16 @@ import ( "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/fluxcd/source-controller/internal/helm/registry" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) { @@ -162,3 +166,148 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) { }) } } + +func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) { + type secretOptions struct { + username string + password string + } + + tests := []struct { + name string + url string + registryOpts registryOptions + secretOpts secretOptions + provider string + providerImg string + want ctrl.Result + wantErr bool + assertConditions []metav1.Condition + }{ + { + name: "HTTP without basic auth", + want: ctrl.Result{RequeueAfter: interval}, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"), + }, + }, + { + name: "HTTP with basic auth secret", + want: ctrl.Result{RequeueAfter: interval}, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: testRegistryUsername, + password: testRegistryPassword, + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"), + }, + }, + { + name: "HTTP registry - basic auth with invalid secret", + want: ctrl.Result{}, + wantErr: true, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: "wrong-pass", + password: "wrong-pass", + }, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to login to registry"), + }, + }, + { + name: "with contextual login provider", + wantErr: true, + provider: "aws", + providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test", + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"), + }, + }, + { + name: "with contextual login provider and secretRef", + want: ctrl.Result{RequeueAfter: interval}, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + secretOpts: secretOptions{ + username: testRegistryUsername, + password: testRegistryPassword, + }, + provider: "azure", + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme()) + workspaceDir := t.TempDir() + server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts) + g.Expect(err).NotTo(HaveOccurred()) + + obj := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "auth-strategy-", + }, + Spec: sourcev1.HelmRepositorySpec{ + Interval: metav1.Duration{Duration: interval}, + Timeout: &metav1.Duration{Duration: timeout}, + Type: sourcev1.HelmRepositoryTypeOCI, + Provider: sourcev1.GenericOCIProvider, + URL: fmt.Sprintf("oci://%s", server.registryHost), + }, + } + + if tt.provider != "" { + obj.Spec.Provider = tt.provider + } + // If a provider specific image is provided, overwrite existing URL + // set earlier. It'll fail but it's necessary to set them because + // the login check expects the URLs to be of certain pattern. + if tt.providerImg != "" { + obj.Spec.URL = tt.providerImg + } + + if tt.secretOpts.username != "" && tt.secretOpts.password != "" { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "auth-secretref", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, + server.registryHost, tt.secretOpts.username, tt.secretOpts.password)), + }, + } + + builder.WithObjects(secret) + + obj.Spec.SecretRef = &meta.LocalObjectReference{ + Name: secret.Name, + } + } + + r := &HelmRepositoryOCIReconciler{ + Client: builder.Build(), + EventRecorder: record.NewFakeRecorder(32), + Getters: testGetters, + RegistryClientGenerator: registry.ClientGenerator, + } + + got, err := r.reconcile(ctx, obj) + g.Expect(err != nil).To(Equal(tt.wantErr)) + g.Expect(got).To(Equal(tt.want)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions)) + }) + } +} diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 1e8744b02..b05c5e8b3 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -308,8 +308,8 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } options = append(options, crane.WithAuthFromKeychain(keychain)) - if obj.Spec.Provider != sourcev1.GenericOCIProvider { - auth, authErr := r.oidcAuth(ctxTimeout, obj) + if _, ok := keychain.(util.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok { + auth, authErr := 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), @@ -589,9 +589,9 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OC } } - // if no pullsecrets available return DefaultKeyChain + // if no pullsecrets available return an AnonymousKeychain if len(pullSecretNames) == 0 { - return authn.DefaultKeychain, nil + return util.Anonymous{}, nil } // lookup image pull secrets @@ -655,15 +655,15 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O } // oidcAuth generates the OIDC credential authenticator based on the specified cloud provider. -func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Authenticator, error) { - url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) - ref, err := name.ParseReference(url) +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", obj.Spec.URL, err) + return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err) } opts := login.ProviderOptions{} - switch obj.Spec.Provider { + switch provider { case sourcev1.AmazonOCIProvider: opts.AwsAutoLogin = true case sourcev1.AzureOCIProvider: @@ -672,7 +672,7 @@ func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OC opts.GcpAutoLogin = true } - return login.NewManager().Login(ctx, url, ref, opts) + return login.NewManager().Login(ctx, u, ref, opts) } // craneOptions sets the auth headers, timeout and user agent diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index b08527bfd..f6fe50118 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -369,6 +369,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { craneOpts []crane.Option secretOpts secretOptions tlsCertSecret *corev1.Secret + provider string + providerImg string want sreconcile.Result wantErr bool assertConditions []metav1.Condition @@ -548,6 +550,36 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), }, }, + { + name: "with contextual login provider", + wantErr: true, + provider: "aws", + providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test", + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"), + }, + }, + { + name: "with contextual login provider and secretRef", + want: sreconcile.ResultSuccess, + registryOpts: registryOptions{ + withBasicAuth: true, + }, + craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + })}, + secretOpts: secretOptions{ + username: testRegistryUsername, + password: testRegistryPassword, + includeSecret: true, + }, + provider: "azure", + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + }, + }, } for _, tt := range tests { @@ -578,6 +610,16 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { Tag: img.tag, } + if tt.provider != "" { + obj.Spec.Provider = tt.provider + } + // If a provider specific image is provided, overwrite existing URL + // set earlier. It'll fail but it's necessary to set them because + // the login check expects the URLs to be of certain pattern. + if tt.providerImg != "" { + obj.Spec.URL = tt.providerImg + } + if tt.secretOpts.username != "" && tt.secretOpts.password != "" { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/suite_test.go b/controllers/suite_test.go index b2956b58c..8654f06f4 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -36,10 +36,12 @@ import ( "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" + dcontext "github.com/distribution/distribution/v3/context" "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/testenv" "github.com/fluxcd/pkg/testserver" "github.com/phayes/freeport" + "github.com/sirupsen/logrus" "github.com/distribution/distribution/v3/configuration" dockerRegistry "github.com/distribution/distribution/v3/registry" @@ -153,8 +155,6 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry server.registryHost = fmt.Sprintf("localhost:%d", port) config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) config.HTTP.DrainTimeout = time.Duration(10) * time.Second - config.Log.AccessLog.Disabled = true - config.Log.Level = "error" config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} if opts.withBasicAuth { @@ -184,6 +184,13 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry config.HTTP.TLS.Key = "testdata/certs/server-key.pem" } + // setup logger options + config.Log.AccessLog.Disabled = true + config.Log.Level = "error" + logger := logrus.New() + logger.SetOutput(io.Discard) + dcontext.SetDefaultLogger(logrus.NewEntry(logger)) + dockerRegistry, err := dockerRegistry.NewRegistry(ctx, config) if err != nil { return nil, fmt.Errorf("failed to create docker registry: %w", err) diff --git a/internal/util/auth.go b/internal/util/auth.go new file mode 100644 index 000000000..8b944cc31 --- /dev/null +++ b/internal/util/auth.go @@ -0,0 +1,30 @@ +/* +Copyright 2022 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 util + +import "github.com/google/go-containerregistry/pkg/authn" + +// Anonymous is an authn.AuthConfig that always returns an anonymous +// authenticator. It is useful for registries that do not require authentication +// or when the credentials are not known. +// It implements authn.Keychain `Resolve` method and can be used as a keychain. +type Anonymous authn.AuthConfig + +// Resolve implements authn.Keychain. +func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) { + return authn.Anonymous, nil +}