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)
|
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
||||||
}
|
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
||||||
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
|
||||||
auth, authErr := oidcAuth(ctxTimeout, repo)
|
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
|
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)
|
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
||||||
}
|
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
||||||
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
|
||||||
auth, authErr := oidcAuth(ctxTimeout, repo)
|
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
|
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"
|
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"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"
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
@ -893,21 +894,11 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
|
||||||
chartPath = "testdata/charts/helmchart-0.1.0.tgz"
|
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
|
// Load a test chart
|
||||||
chartData, err := ioutil.ReadFile(chartPath)
|
chartData, err := ioutil.ReadFile(chartPath)
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
|
||||||
metadata, err := extractChartMeta(chartData)
|
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
// Upload the test chart
|
// Upload the test chart
|
||||||
ref := fmt.Sprintf("%s/testrepo/%s:%s", testRegistryServer.registryHost, metadata.Name, metadata.Version)
|
metadata, err := loadTestChartToOCI(chartData, chartPath, testRegistryServer)
|
||||||
_, err = testRegistryServer.registryClient.Push(chartData, ref)
|
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
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
|
// extractChartMeta is used to extract a chart metadata from a byte array
|
||||||
func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
||||||
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
|
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
|
||||||
|
@ -2046,3 +2225,32 @@ func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
||||||
}
|
}
|
||||||
return ch.Metadata, nil
|
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"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
|
@ -42,12 +41,10 @@ import (
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/oci"
|
"github.com/fluxcd/pkg/oci"
|
||||||
"github.com/fluxcd/pkg/oci/auth/login"
|
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
"github.com/fluxcd/pkg/runtime/predicates"
|
"github.com/fluxcd/pkg/runtime/predicates"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/api/v1beta2"
|
"github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
sourcev1 "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 {
|
if loginOpt != nil {
|
||||||
loginOpts = append(loginOpts, loginOpt)
|
loginOpts = append(loginOpts, loginOpt)
|
||||||
}
|
}
|
||||||
}
|
} else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuthFromAdapter(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
|
||||||
auth, authErr := oidcAuth(ctxTimeout, obj)
|
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
@ -380,41 +375,12 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
|
||||||
r.Eventf(obj, eventType, reason, msg)
|
r.Eventf(obj, eventType, reason, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
// oidcAuthFromAdapter generates the OIDC credential authenticator based on the specified cloud provider.
|
||||||
func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) {
|
func oidcAuthFromAdapter(ctx context.Context, url, provider string) (helmreg.LoginOption, error) {
|
||||||
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
|
auth, err := oidcAuth(ctx, url, provider)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return registry.OIDCAdaptHelper(auth)
|
return registry.OIDCAdaptHelper(auth)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,16 @@ import (
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/tools/record"
|
||||||
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
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))
|
options = append(options, crane.WithAuthFromKeychain(keychain))
|
||||||
|
|
||||||
if obj.Spec.Provider != sourcev1.GenericOCIProvider {
|
if _, ok := keychain.(util.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
|
||||||
auth, authErr := r.oidcAuth(ctxTimeout, obj)
|
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := serror.NewGeneric(
|
e := serror.NewGeneric(
|
||||||
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
|
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
|
||||||
|
@ -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 {
|
if len(pullSecretNames) == 0 {
|
||||||
return authn.DefaultKeychain, nil
|
return util.Anonymous{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup image pull secrets
|
// 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.
|
// 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) {
|
func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
|
||||||
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
|
u := strings.TrimPrefix(url, sourcev1.OCIRepositoryPrefix)
|
||||||
ref, err := name.ParseReference(url)
|
ref, err := name.ParseReference(u)
|
||||||
if err != nil {
|
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{}
|
opts := login.ProviderOptions{}
|
||||||
switch obj.Spec.Provider {
|
switch provider {
|
||||||
case sourcev1.AmazonOCIProvider:
|
case sourcev1.AmazonOCIProvider:
|
||||||
opts.AwsAutoLogin = true
|
opts.AwsAutoLogin = true
|
||||||
case sourcev1.AzureOCIProvider:
|
case sourcev1.AzureOCIProvider:
|
||||||
|
@ -672,7 +672,7 @@ func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OC
|
||||||
opts.GcpAutoLogin = true
|
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
|
// craneOptions sets the auth headers, timeout and user agent
|
||||||
|
|
|
@ -369,6 +369,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
|
||||||
craneOpts []crane.Option
|
craneOpts []crane.Option
|
||||||
secretOpts secretOptions
|
secretOpts secretOptions
|
||||||
tlsCertSecret *corev1.Secret
|
tlsCertSecret *corev1.Secret
|
||||||
|
provider string
|
||||||
|
providerImg string
|
||||||
want sreconcile.Result
|
want sreconcile.Result
|
||||||
wantErr bool
|
wantErr bool
|
||||||
assertConditions []metav1.Condition
|
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 "),
|
*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 {
|
for _, tt := range tests {
|
||||||
|
@ -578,6 +610,16 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
|
||||||
Tag: img.tag,
|
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 != "" {
|
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
|
||||||
secret := &corev1.Secret{
|
secret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|
|
@ -36,10 +36,12 @@ import (
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
|
||||||
|
dcontext "github.com/distribution/distribution/v3/context"
|
||||||
"github.com/fluxcd/pkg/runtime/controller"
|
"github.com/fluxcd/pkg/runtime/controller"
|
||||||
"github.com/fluxcd/pkg/runtime/testenv"
|
"github.com/fluxcd/pkg/runtime/testenv"
|
||||||
"github.com/fluxcd/pkg/testserver"
|
"github.com/fluxcd/pkg/testserver"
|
||||||
"github.com/phayes/freeport"
|
"github.com/phayes/freeport"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3/configuration"
|
"github.com/distribution/distribution/v3/configuration"
|
||||||
dockerRegistry "github.com/distribution/distribution/v3/registry"
|
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)
|
server.registryHost = fmt.Sprintf("localhost:%d", port)
|
||||||
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
|
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
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{}{}}
|
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
|
||||||
|
|
||||||
if opts.withBasicAuth {
|
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"
|
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)
|
dockerRegistry, err := dockerRegistry.NewRegistry(ctx, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create docker registry: %w", err)
|
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