Skip to content

Commit 21bbb5c

Browse files
authored
Merge pull request fluxcd#873 from souleb/enable-oidc-auth
Enable contextual login in OCI HelmRepository
2 parents 2010eef + ad3eb5c commit 21bbb5c

File tree

11 files changed

+363
-9
lines changed

11 files changed

+363
-9
lines changed

api/v1beta2/helmrepository_types.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ type HelmRepositorySpec struct {
6868
// +required
6969
Interval metav1.Duration `json:"interval"`
7070

71-
// Timeout of the index fetch operation, defaults to 60s.
71+
// Timeout is used for the index fetch operation for an HTTPS helm repository,
72+
// and for remote OCI Repository operations like pulling for an OCI helm repository.
73+
// Its default value is 60s.
7274
// +kubebuilder:default:="60s"
7375
// +optional
7476
Timeout *metav1.Duration `json:"timeout,omitempty"`
@@ -89,6 +91,14 @@ type HelmRepositorySpec struct {
8991
// +kubebuilder:validation:Enum=default;oci
9092
// +optional
9193
Type string `json:"type,omitempty"`
94+
95+
// Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
96+
// This field is optional, and only taken into account if the .spec.type field is set to 'oci'.
97+
// When not specified, defaults to 'generic'.
98+
// +kubebuilder:validation:Enum=generic;aws;azure;gcp
99+
// +kubebuilder:default:=generic
100+
// +optional
101+
Provider string `json:"provider,omitempty"`
92102
}
93103

94104
// HelmRepositoryStatus records the observed state of the HelmRepository.

config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,18 @@ spec:
310310
be done with caution, as it can potentially result in credentials
311311
getting stolen in a MITM-attack.
312312
type: boolean
313+
provider:
314+
default: generic
315+
description: Provider used for authentication, can be 'aws', 'azure',
316+
'gcp' or 'generic'. This field is optional, and only taken into
317+
account if the .spec.type field is set to 'oci'. When not specified,
318+
defaults to 'generic'.
319+
enum:
320+
- generic
321+
- aws
322+
- azure
323+
- gcp
324+
type: string
313325
secretRef:
314326
description: SecretRef specifies the Secret containing authentication
315327
credentials for the HelmRepository. For HTTP/S basic auth the secret
@@ -328,7 +340,9 @@ spec:
328340
type: boolean
329341
timeout:
330342
default: 60s
331-
description: Timeout of the index fetch operation, defaults to 60s.
343+
description: Timeout is used for the index fetch operation for an
344+
HTTPS helm repository, and for remote OCI Repository operations
345+
like pulling for an OCI helm repository. Its default value is 60s.
332346
type: string
333347
type:
334348
description: Type of the HelmRepository. When this field is set to "oci",

controllers/helmchart_controller.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
"sigs.k8s.io/controller-runtime/pkg/source"
5151

5252
"github.com/fluxcd/pkg/apis/meta"
53+
"github.com/fluxcd/pkg/oci"
5354
"github.com/fluxcd/pkg/runtime/conditions"
5455
helper "github.com/fluxcd/pkg/runtime/controller"
5556
"github.com/fluxcd/pkg/runtime/events"
@@ -463,6 +464,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
463464
tlsConfig *tls.Config
464465
loginOpts []helmreg.LoginOption
465466
)
467+
// Used to login with the repository declared provider
468+
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
469+
defer cancel()
466470

467471
normalizedURL := repository.NormalizeURL(repo.Spec.URL)
468472
// Construct the Getter options from the HelmRepository data
@@ -521,6 +525,21 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
521525
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
522526
}
523527

528+
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
529+
auth, authErr := oidcAuth(ctxTimeout, repo)
530+
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
531+
e := &serror.Event{
532+
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
533+
Reason: sourcev1.AuthenticationFailedReason,
534+
}
535+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
536+
return sreconcile.ResultEmpty, e
537+
}
538+
if auth != nil {
539+
loginOpts = append([]helmreg.LoginOption{}, auth)
540+
}
541+
}
542+
524543
// Initialize the chart repository
525544
var chartRepo repository.Downloader
526545
switch repo.Spec.Type {
@@ -947,6 +966,11 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
947966
},
948967
}
949968
}
969+
970+
// Used to login with the repository declared provider
971+
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
972+
defer cancel()
973+
950974
clientOpts := []helmgetter.Option{
951975
helmgetter.WithURL(normalizedURL),
952976
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
@@ -976,6 +1000,16 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
9761000
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
9771001
}
9781002

1003+
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
1004+
auth, authErr := oidcAuth(ctxTimeout, repo)
1005+
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
1006+
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
1007+
}
1008+
if auth != nil {
1009+
loginOpts = append([]helmreg.LoginOption{}, auth)
1010+
}
1011+
}
1012+
9791013
var chartRepo repository.Downloader
9801014
if helmreg.IsOCI(normalizedURL) {
9811015
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil)

controllers/helmchart_controller_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,9 +1085,10 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
10851085
GenerateName: "helmrepository-",
10861086
},
10871087
Spec: sourcev1.HelmRepositorySpec{
1088-
URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost),
1089-
Timeout: &metav1.Duration{Duration: timeout},
1090-
Type: sourcev1.HelmRepositoryTypeOCI,
1088+
URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost),
1089+
Timeout: &metav1.Duration{Duration: timeout},
1090+
Provider: sourcev1.GenericOCIProvider,
1091+
Type: sourcev1.HelmRepositoryTypeOCI,
10911092
},
10921093
}
10931094
obj := &sourcev1.HelmChart{

controllers/helmrepository_controller_oci.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"net/url"
2424
"os"
25+
"strings"
2526
"time"
2627

2728
helmgetter "helm.sh/helm/v3/pkg/getter"
@@ -41,10 +42,13 @@ import (
4142
"sigs.k8s.io/controller-runtime/pkg/predicate"
4243

4344
"github.com/fluxcd/pkg/apis/meta"
45+
"github.com/fluxcd/pkg/oci"
46+
"github.com/fluxcd/pkg/oci/auth/login"
4447
"github.com/fluxcd/pkg/runtime/conditions"
4548
helper "github.com/fluxcd/pkg/runtime/controller"
4649
"github.com/fluxcd/pkg/runtime/patch"
4750
"github.com/fluxcd/pkg/runtime/predicates"
51+
"github.com/google/go-containerregistry/pkg/name"
4852

4953
"github.com/fluxcd/source-controller/api/v1beta2"
5054
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@@ -204,6 +208,9 @@ func (r *HelmRepositoryOCIReconciler) Reconcile(ctx context.Context, req ctrl.Re
204208
// block at the very end to summarize the conditions to be in a consistent
205209
// state.
206210
func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta2.HelmRepository) (result ctrl.Result, retErr error) {
211+
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
212+
defer cancel()
213+
207214
oldObj := obj.DeepCopy()
208215

209216
defer func() {
@@ -296,6 +303,19 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
296303
}
297304
}
298305

306+
if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
307+
auth, authErr := oidcAuth(ctxTimeout, obj)
308+
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
309+
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
310+
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
311+
result, retErr = ctrl.Result{}, e
312+
return
313+
}
314+
if auth != nil {
315+
loginOpts = append(loginOpts, auth)
316+
}
317+
}
318+
299319
// Create registry client and login if needed.
300320
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
301321
if err != nil {
@@ -366,3 +386,42 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
366386
}
367387
r.Eventf(obj, eventType, reason, msg)
368388
}
389+
390+
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
391+
func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) {
392+
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
393+
ref, err := name.ParseReference(url)
394+
if err != nil {
395+
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
396+
}
397+
398+
loginOpt, err := loginWithManager(ctx, obj.Spec.Provider, url, ref)
399+
if err != nil {
400+
return nil, fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
401+
}
402+
403+
return loginOpt, nil
404+
}
405+
406+
func loginWithManager(ctx context.Context, provider, url string, ref name.Reference) (helmreg.LoginOption, error) {
407+
opts := login.ProviderOptions{}
408+
switch provider {
409+
case sourcev1.AmazonOCIProvider:
410+
opts.AwsAutoLogin = true
411+
case sourcev1.AzureOCIProvider:
412+
opts.AzureAutoLogin = true
413+
case sourcev1.GoogleOCIProvider:
414+
opts.GcpAutoLogin = true
415+
}
416+
417+
auth, err := login.NewManager().Login(ctx, url, ref, opts)
418+
if err != nil {
419+
return nil, err
420+
}
421+
422+
if auth == nil {
423+
return nil, nil
424+
}
425+
426+
return registry.OIDCAdaptHelper(auth)
427+
}

controllers/helmrepository_controller_oci_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
9494
SecretRef: &meta.LocalObjectReference{
9595
Name: secret.Name,
9696
},
97-
Type: sourcev1.HelmRepositoryTypeOCI,
97+
Provider: sourcev1.GenericOCIProvider,
98+
Type: sourcev1.HelmRepositoryTypeOCI,
9899
},
99100
}
100101
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())

docs/api/source.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,9 @@ Kubernetes meta/v1.Duration
818818
</td>
819819
<td>
820820
<em>(Optional)</em>
821-
<p>Timeout of the index fetch operation, defaults to 60s.</p>
821+
<p>Timeout is used for the index fetch operation for an HTTPS helm repository,
822+
and for remote OCI Repository operations like pulling for an OCI helm repository.
823+
Its default value is 60s.</p>
822824
</td>
823825
</tr>
824826
<tr>
@@ -863,6 +865,20 @@ string
863865
When this field is set to &ldquo;oci&rdquo;, the URL field value must be prefixed with &ldquo;oci://&rdquo;.</p>
864866
</td>
865867
</tr>
868+
<tr>
869+
<td>
870+
<code>provider</code><br>
871+
<em>
872+
string
873+
</em>
874+
</td>
875+
<td>
876+
<em>(Optional)</em>
877+
<p>Provider used for authentication, can be &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
878+
This field is optional, and only taken into account if the .spec.type field is set to &lsquo;oci&rsquo;.
879+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
880+
</td>
881+
</tr>
866882
</table>
867883
</td>
868884
</tr>
@@ -2347,7 +2363,9 @@ Kubernetes meta/v1.Duration
23472363
</td>
23482364
<td>
23492365
<em>(Optional)</em>
2350-
<p>Timeout of the index fetch operation, defaults to 60s.</p>
2366+
<p>Timeout is used for the index fetch operation for an HTTPS helm repository,
2367+
and for remote OCI Repository operations like pulling for an OCI helm repository.
2368+
Its default value is 60s.</p>
23512369
</td>
23522370
</tr>
23532371
<tr>
@@ -2392,6 +2410,20 @@ string
23922410
When this field is set to &ldquo;oci&rdquo;, the URL field value must be prefixed with &ldquo;oci://&rdquo;.</p>
23932411
</td>
23942412
</tr>
2413+
<tr>
2414+
<td>
2415+
<code>provider</code><br>
2416+
<em>
2417+
string
2418+
</em>
2419+
</td>
2420+
<td>
2421+
<em>(Optional)</em>
2422+
<p>Provider used for authentication, can be &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
2423+
This field is optional, and only taken into account if the .spec.type field is set to &lsquo;oci&rsquo;.
2424+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
2425+
</td>
2426+
</tr>
23952427
</tbody>
23962428
</table>
23972429
</div>

0 commit comments

Comments
 (0)