Skip to content

Commit ad3eb5c

Browse files
committed
Enable contextual login for helm OCI
If implemented, this pr will enable user to use the auto login feature in order to automatically login to their provider of choice's container registry (i.e. aws, gcr, acr). Signed-off-by: Soule BA <[email protected]>
1 parent 2010eef commit ad3eb5c

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)