Skip to content

Commit b7f09b6

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 <soule@weave.works>
1 parent 02be5de commit b7f09b6

9 files changed

+166
-4
lines changed

Makefile

-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ docker-build: ## Build the Docker image
144144
docker buildx build \
145145
--build-arg LIBGIT2_IMG=$(LIBGIT2_IMG) \
146146
--build-arg LIBGIT2_TAG=$(LIBGIT2_TAG) \
147-
--platform=$(BUILD_PLATFORMS) \
148147
-t $(IMG):$(TAG) \
149148
$(BUILD_ARGS) .
150149

api/v1beta2/helmrepository_types.go

+8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ type HelmRepositorySpec struct {
4646
// +required
4747
URL string `json:"url"`
4848

49+
// The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
50+
// This field is optional and if used, the type field must be set to "oci".
51+
// When not specified, defaults to 'generic'.
52+
// +kubebuilder:validation:Enum=generic;aws;azure;gcp
53+
// +kubebuilder:default:=generic
54+
// +optional
55+
Provider string `json:"provider,omitempty"`
56+
4957
// SecretRef specifies the Secret containing authentication credentials
5058
// for the HelmRepository.
5159
// For HTTP/S basic auth the secret must contain 'username' and 'password'

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

+11
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ 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: The provider used for authentication, can be 'aws', 'azure',
316+
'gcp' or 'generic'. This field is optional and if used, the type
317+
field must be set to "oci". When not specified, defaults to 'generic'.
318+
enum:
319+
- generic
320+
- aws
321+
- azure
322+
- gcp
323+
type: string
313324
secretRef:
314325
description: SecretRef specifies the Secret containing authentication
315326
credentials for the HelmRepository. For HTTP/S basic auth the secret

controllers/helmchart_controller.go

+34
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.NewGeneric(
532+
fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
533+
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

+4-3
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

+60
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,20 @@ 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 :=
310+
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
311+
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
312+
result, retErr = ctrl.Result{}, e
313+
return
314+
}
315+
if auth != nil {
316+
loginOpts = append(loginOpts, auth)
317+
}
318+
}
319+
299320
// Create registry client and login if needed.
300321
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
301322
if err != nil {
@@ -366,3 +387,42 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
366387
}
367388
r.Eventf(obj, eventType, reason, msg)
368389
}
390+
391+
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
392+
func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) {
393+
url := strings.TrimPrefix(obj.Spec.URL, helmreg.OCIScheme)
394+
ref, err := name.ParseReference(url)
395+
if err != nil {
396+
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
397+
}
398+
399+
loginOpt, err := loginWithManager(ctx, obj.Spec.Provider, url, ref)
400+
if err != nil {
401+
return nil, fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
402+
}
403+
404+
return loginOpt, nil
405+
}
406+
407+
func loginWithManager(ctx context.Context, provider, url string, ref name.Reference) (helmreg.LoginOption, error) {
408+
opts := login.ProviderOptions{}
409+
switch provider {
410+
case sourcev1.AmazonOCIProvider:
411+
opts.AwsAutoLogin = true
412+
case sourcev1.AzureOCIProvider:
413+
opts.AzureAutoLogin = true
414+
case sourcev1.GoogleOCIProvider:
415+
opts.GcpAutoLogin = true
416+
}
417+
418+
auth, err := login.NewManager().Login(ctx, url, ref, opts)
419+
if err != nil {
420+
return nil, err
421+
}
422+
423+
if auth == nil {
424+
return nil, nil
425+
}
426+
427+
return registry.OIDCAdaptHelper(auth)
428+
}

controllers/helmrepository_controller_oci_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
9494
SecretRef: &meta.LocalObjectReference{
9595
Name: secret.Name,
9696
},
97+
Provider: sourcev1.GenericOCIProvider,
9798
Type: sourcev1.HelmRepositoryTypeOCI,
9899
},
99100
}

docs/api/source.md

+28
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,20 @@ host.</p>
760760
</tr>
761761
<tr>
762762
<td>
763+
<code>provider</code><br>
764+
<em>
765+
string
766+
</em>
767+
</td>
768+
<td>
769+
<em>(Optional)</em>
770+
<p>The provider used for authentication, can be &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
771+
This field is optional and if used, the type field must be set to &ldquo;oci&rdquo;.
772+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
773+
</td>
774+
</tr>
775+
<tr>
776+
<td>
763777
<code>secretRef</code><br>
764778
<em>
765779
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@@ -2274,6 +2288,20 @@ host.</p>
22742288
</tr>
22752289
<tr>
22762290
<td>
2291+
<code>provider</code><br>
2292+
<em>
2293+
string
2294+
</em>
2295+
</td>
2296+
<td>
2297+
<em>(Optional)</em>
2298+
<p>The provider used for authentication, can be &lsquo;aws&rsquo;, &lsquo;azure&rsquo;, &lsquo;gcp&rsquo; or &lsquo;generic&rsquo;.
2299+
This field is optional and if used, the type field must be set to &ldquo;oci&rdquo;.
2300+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
2301+
</td>
2302+
</tr>
2303+
<tr>
2304+
<td>
22772305
<code>secretRef</code><br>
22782306
<em>
22792307
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">

internal/helm/registry/auth.go

+20
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/docker/cli/cli/config"
2525
"github.com/docker/cli/cli/config/credentials"
26+
"github.com/google/go-containerregistry/pkg/authn"
2627
"helm.sh/helm/v3/pkg/registry"
2728
corev1 "k8s.io/api/core/v1"
2829
)
@@ -68,3 +69,22 @@ func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.L
6869
}
6970
return registry.LoginOptBasicAuth(username, password), nil
7071
}
72+
73+
// OIDCAdaptHelper adapt crane authenticator to oras credential function
74+
func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, error) {
75+
authConfig, err := authenticator.Authorization()
76+
if err != nil {
77+
return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err)
78+
}
79+
80+
username := authConfig.Username
81+
password := authConfig.Password
82+
83+
switch {
84+
case username == "" && password == "":
85+
return nil, nil
86+
case username == "" || password == "":
87+
return nil, fmt.Errorf("invalid auth data: required fields 'username' and 'password'")
88+
}
89+
return registry.LoginOptBasicAuth(username, password), nil
90+
}

0 commit comments

Comments
 (0)