diff --git a/api/v1beta2/helmrepository_types.go b/api/v1beta2/helmrepository_types.go index 87c0b16b..d9d72b0b 100644 --- a/api/v1beta2/helmrepository_types.go +++ b/api/v1beta2/helmrepository_types.go @@ -68,7 +68,9 @@ type HelmRepositorySpec struct { // +required Interval metav1.Duration `json:"interval"` - // Timeout of the index fetch operation, defaults to 60s. + // Timeout is used for the index fetch operation for an HTTPS helm repository, + // and for remote OCI Repository operations like pulling for an OCI helm repository. + // Its default value is 60s. // +kubebuilder:default:="60s" // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` @@ -89,6 +91,14 @@ type HelmRepositorySpec struct { // +kubebuilder:validation:Enum=default;oci // +optional Type string `json:"type,omitempty"` + + // Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + // This field is optional, and only taken into account if the .spec.type field is set to 'oci'. + // When not specified, defaults to 'generic'. + // +kubebuilder:validation:Enum=generic;aws;azure;gcp + // +kubebuilder:default:=generic + // +optional + Provider string `json:"provider,omitempty"` } // HelmRepositoryStatus records the observed state of the HelmRepository. diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml index c19552fd..3aba3cf9 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml @@ -310,6 +310,18 @@ spec: be done with caution, as it can potentially result in credentials getting stolen in a MITM-attack. type: boolean + provider: + default: generic + description: Provider used for authentication, can be 'aws', 'azure', + 'gcp' or 'generic'. This field is optional, and only taken into + account if the .spec.type field is set to 'oci'. When not specified, + defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string secretRef: description: SecretRef specifies the Secret containing authentication credentials for the HelmRepository. For HTTP/S basic auth the secret @@ -328,7 +340,9 @@ spec: type: boolean timeout: default: 60s - description: Timeout of the index fetch operation, defaults to 60s. + description: Timeout is used for the index fetch operation for an + HTTPS helm repository, and for remote OCI Repository operations + like pulling for an OCI helm repository. Its default value is 60s. type: string type: description: Type of the HelmRepository. When this field is set to "oci", diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 032f678b..fd171472 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -50,6 +50,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/oci" "github.com/fluxcd/pkg/runtime/conditions" helper "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/events" @@ -463,6 +464,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * tlsConfig *tls.Config loginOpts []helmreg.LoginOption ) + // Used to login with the repository declared provider + ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration) + defer cancel() normalizedURL := repository.NormalizeURL(repo.Spec.URL) // Construct the Getter options from the HelmRepository data @@ -521,6 +525,21 @@ 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) + 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 { + loginOpts = append([]helmreg.LoginOption{}, auth) + } + } + // Initialize the chart repository var chartRepo repository.Downloader switch repo.Spec.Type { @@ -947,6 +966,11 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont }, } } + + // Used to login with the repository declared provider + ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration) + defer cancel() + clientOpts := []helmgetter.Option{ helmgetter.WithURL(normalizedURL), helmgetter.WithTimeout(repo.Spec.Timeout.Duration), @@ -976,6 +1000,16 @@ 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) + if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { + return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr) + } + if auth != nil { + loginOpts = append([]helmreg.LoginOption{}, auth) + } + } + var chartRepo repository.Downloader if helmreg.IsOCI(normalizedURL) { registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil) diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 26e771c5..e9c3920d 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -1085,9 +1085,10 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) { GenerateName: "helmrepository-", }, Spec: sourcev1.HelmRepositorySpec{ - URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost), - Timeout: &metav1.Duration{Duration: timeout}, - Type: sourcev1.HelmRepositoryTypeOCI, + URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost), + Timeout: &metav1.Duration{Duration: timeout}, + Provider: sourcev1.GenericOCIProvider, + Type: sourcev1.HelmRepositoryTypeOCI, }, } obj := &sourcev1.HelmChart{ diff --git a/controllers/helmrepository_controller_oci.go b/controllers/helmrepository_controller_oci.go index a7d812fa..75f1ccbf 100644 --- a/controllers/helmrepository_controller_oci.go +++ b/controllers/helmrepository_controller_oci.go @@ -22,6 +22,7 @@ import ( "fmt" "net/url" "os" + "strings" "time" helmgetter "helm.sh/helm/v3/pkg/getter" @@ -41,10 +42,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "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" @@ -204,6 +208,9 @@ func (r *HelmRepositoryOCIReconciler) Reconcile(ctx context.Context, req ctrl.Re // block at the very end to summarize the conditions to be in a consistent // state. func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta2.HelmRepository) (result ctrl.Result, retErr error) { + ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) + defer cancel() + oldObj := obj.DeepCopy() defer func() { @@ -296,6 +303,19 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta } } + if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + auth, authErr := oidcAuth(ctxTimeout, obj) + 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()) + result, retErr = ctrl.Result{}, e + return + } + if auth != nil { + loginOpts = append(loginOpts, auth) + } + } + // Create registry client and login if needed. registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil) if err != nil { @@ -366,3 +386,42 @@ 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) + 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 62d49ec2..ec75a67e 100644 --- a/controllers/helmrepository_controller_oci_test.go +++ b/controllers/helmrepository_controller_oci_test.go @@ -94,7 +94,8 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) { SecretRef: &meta.LocalObjectReference{ Name: secret.Name, }, - Type: sourcev1.HelmRepositoryTypeOCI, + Provider: sourcev1.GenericOCIProvider, + Type: sourcev1.HelmRepositoryTypeOCI, }, } g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) diff --git a/docs/api/source.md b/docs/api/source.md index ec0b1daf..47368ddc 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -818,7 +818,9 @@ Kubernetes meta/v1.Duration
Timeout of the index fetch operation, defaults to 60s.
+Timeout is used for the index fetch operation for an HTTPS helm repository, +and for remote OCI Repository operations like pulling for an OCI helm repository. +Its default value is 60s.
provider
Provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’. +This field is optional, and only taken into account if the .spec.type field is set to ‘oci’. +When not specified, defaults to ‘generic’.
+Timeout of the index fetch operation, defaults to 60s.
+Timeout is used for the index fetch operation for an HTTPS helm repository, +and for remote OCI Repository operations like pulling for an OCI helm repository. +Its default value is 60s.
provider
Provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’. +This field is optional, and only taken into account if the .spec.type field is set to ‘oci’. +When not specified, defaults to ‘generic’.
+