Merge pull request #884 from souleb/fix-874
[OCI] Static credentials should take precedence over the OIDC provider
This commit is contained in:
commit
cf0e9ac2fe
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 '<helmchart>' chart with version '<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 '<helmchart>' chart with version '<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 '<helmchart>' chart with version '<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, "<helmchart>", metadata.Name)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<version>", 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '<digest>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue