Skip to content

Commit 351b21b

Browse files
authored
Merge pull request #725 from fluxcd/oci-dockerconfig
Support dockerconfigjson with OCI HelmRepositories
2 parents a901233 + bb569be commit 351b21b

10 files changed

+241
-143
lines changed

controllers/helmchart_controller.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"time"
3030

3131
helmgetter "helm.sh/helm/v3/pkg/getter"
32-
"helm.sh/helm/v3/pkg/registry"
32+
helmreg "helm.sh/helm/v3/pkg/registry"
3333
corev1 "k8s.io/api/core/v1"
3434
apierrs "k8s.io/apimachinery/pkg/api/errors"
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -60,6 +60,7 @@ import (
6060
serror "github.com/fluxcd/source-controller/internal/error"
6161
"github.com/fluxcd/source-controller/internal/helm/chart"
6262
"github.com/fluxcd/source-controller/internal/helm/getter"
63+
"github.com/fluxcd/source-controller/internal/helm/registry"
6364
"github.com/fluxcd/source-controller/internal/helm/repository"
6465
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
6566
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
@@ -380,7 +381,7 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
380381

381382
// Assert source has an artifact
382383
if s.GetArtifact() == nil || !r.Storage.ArtifactExist(*s.GetArtifact()) {
383-
if helmRepo, ok := s.(*sourcev1.HelmRepository); !ok || !registry.IsOCI(helmRepo.Spec.URL) {
384+
if helmRepo, ok := s.(*sourcev1.HelmRepository); !ok || !helmreg.IsOCI(helmRepo.Spec.URL) {
384385
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "NoSourceArtifact",
385386
"no artifact available for %s source '%s'", obj.Spec.SourceRef.Kind, obj.Spec.SourceRef.Name)
386387
r.eventLogf(ctx, obj, events.EventTypeTrace, "NoSourceArtifact",
@@ -447,7 +448,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
447448
repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
448449
var (
449450
tlsConfig *tls.Config
450-
logOpts []registry.LoginOption
451+
loginOpts []helmreg.LoginOption
451452
)
452453

453454
// Construct the Getter options from the HelmRepository data
@@ -492,7 +493,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
492493
}
493494

494495
// Build registryClient options from secret
495-
logOpt, err := loginOptionFromSecret(*secret)
496+
loginOpt, err := registry.LoginOptionFromSecret(repo.Spec.URL, *secret)
496497
if err != nil {
497498
e := &serror.Event{
498499
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@@ -503,14 +504,14 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
503504
return sreconcile.ResultEmpty, e
504505
}
505506

506-
logOpts = append([]registry.LoginOption{}, logOpt)
507+
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
507508
}
508509

509510
// Initialize the chart repository
510511
var chartRepo chart.Remote
511512
switch repo.Spec.Type {
512513
case sourcev1.HelmRepositoryTypeOCI:
513-
if !registry.IsOCI(repo.Spec.URL) {
514+
if !helmreg.IsOCI(repo.Spec.URL) {
514515
err := fmt.Errorf("invalid OCI registry URL: %s", repo.Spec.URL)
515516
return chartRepoErrorReturn(err, obj)
516517
}
@@ -519,7 +520,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
519520
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
520521
// TODO@souleb: remove this once the registry move to Oras v2
521522
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
522-
registryClient, file, err := r.RegistryClientGenerator(logOpts != nil)
523+
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
523524
if err != nil {
524525
return chartRepoErrorReturn(err, obj)
525526
}
@@ -540,14 +541,13 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
540541

541542
// If login options are configured, use them to login to the registry
542543
// The OCIGetter will later retrieve the stored credentials to pull the chart
543-
if logOpts != nil {
544-
err = ociChartRepo.Login(logOpts...)
544+
if loginOpts != nil {
545+
err = ociChartRepo.Login(loginOpts...)
545546
if err != nil {
546547
return chartRepoErrorReturn(err, obj)
547548
}
548549
}
549550
default:
550-
var httpChartRepo *repository.ChartRepository
551551
httpChartRepo, err := repository.NewChartRepository(repo.Spec.URL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts,
552552
repository.WithMemoryCache(r.Storage.LocalPath(*repo.GetArtifact()), r.Cache, r.TTL, func(event string) {
553553
r.IncCacheEvents(event, obj.Name, obj.Namespace)

controllers/helmchart_controller_test.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controllers
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/base64"
2223
"errors"
2324
"fmt"
2425
"io"
@@ -35,7 +36,7 @@ import (
3536
. "github.com/onsi/gomega"
3637
hchart "helm.sh/helm/v3/pkg/chart"
3738
"helm.sh/helm/v3/pkg/chart/loader"
38-
"helm.sh/helm/v3/pkg/registry"
39+
helmreg "helm.sh/helm/v3/pkg/registry"
3940
corev1 "k8s.io/api/core/v1"
4041
apierrors "k8s.io/apimachinery/pkg/api/errors"
4142
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -53,7 +54,7 @@ import (
5354
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
5455
serror "github.com/fluxcd/source-controller/internal/error"
5556
"github.com/fluxcd/source-controller/internal/helm/chart"
56-
"github.com/fluxcd/source-controller/internal/helm/util"
57+
"github.com/fluxcd/source-controller/internal/helm/registry"
5758
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
5859
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
5960
)
@@ -792,8 +793,8 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
792793

793794
// Login to the registry
794795
err := testRegistryserver.RegistryClient.Login(testRegistryserver.DockerRegistryHost,
795-
registry.LoginOptBasicAuth(testUsername, testPassword),
796-
registry.LoginOptInsecure(true))
796+
helmreg.LoginOptBasicAuth(testUsername, testPassword),
797+
helmreg.LoginOptInsecure(true))
797798
g.Expect(err).NotTo(HaveOccurred())
798799

799800
// Load a test chart
@@ -825,6 +826,35 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
825826
assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build)
826827
cleanFunc func(g *WithT, build *chart.Build)
827828
}{
829+
{
830+
name: "Reconciles chart build with docker repository credentials",
831+
secret: &corev1.Secret{
832+
ObjectMeta: metav1.ObjectMeta{
833+
Name: "auth",
834+
},
835+
Type: corev1.SecretTypeDockerConfigJson,
836+
Data: map[string][]byte{
837+
".dockerconfigjson": []byte(`{"auths":{"` +
838+
testRegistryserver.DockerRegistryHost + `":{"` +
839+
`auth":"` + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword)) + `"}}}`),
840+
},
841+
},
842+
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
843+
obj.Spec.Chart = metadata.Name
844+
obj.Spec.Version = metadata.Version
845+
repository.Spec.SecretRef = &meta.LocalObjectReference{Name: "auth"}
846+
},
847+
want: sreconcile.ResultSuccess,
848+
assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) {
849+
g.Expect(build.Name).To(Equal(metadata.Name))
850+
g.Expect(build.Version).To(Equal(metadata.Version))
851+
g.Expect(build.Path).ToNot(BeEmpty())
852+
g.Expect(build.Path).To(BeARegularFile())
853+
},
854+
cleanFunc: func(g *WithT, build *chart.Build) {
855+
g.Expect(os.Remove(build.Path)).To(Succeed())
856+
},
857+
},
828858
{
829859
name: "Reconciles chart build with repository credentials",
830860
secret: &corev1.Secret{
@@ -945,7 +975,7 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
945975
EventRecorder: record.NewFakeRecorder(32),
946976
Getters: testGetters,
947977
Storage: storage,
948-
RegistryClientGenerator: util.RegistryClientGenerator,
978+
RegistryClientGenerator: registry.ClientGenerator,
949979
}
950980

951981
repository := &sourcev1.HelmRepository{

controllers/helmrepository_controller_oci.go

+10-18
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ import (
3030
"github.com/fluxcd/pkg/runtime/predicates"
3131
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
3232
serror "github.com/fluxcd/source-controller/internal/error"
33+
"github.com/fluxcd/source-controller/internal/helm/registry"
3334
"github.com/fluxcd/source-controller/internal/helm/repository"
3435
intpredicates "github.com/fluxcd/source-controller/internal/predicates"
3536
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
3637
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
3738
helmgetter "helm.sh/helm/v3/pkg/getter"
38-
"helm.sh/helm/v3/pkg/registry"
39+
helmreg "helm.sh/helm/v3/pkg/registry"
3940
corev1 "k8s.io/api/core/v1"
4041
"k8s.io/apimachinery/pkg/types"
4142
kuberecorder "k8s.io/client-go/tools/record"
@@ -91,7 +92,7 @@ type HelmRepositoryOCIReconciler struct {
9192
// and an optional file name.
9293
// The file is used to store the registry client credentials.
9394
// The caller is responsible for deleting the file.
94-
type RegistryClientGeneratorFunc func(isLogin bool) (*registry.Client, string, error)
95+
type RegistryClientGeneratorFunc func(isLogin bool) (*helmreg.Client, string, error)
9596

9697
// helmRepositoryOCIReconcileFunc is the function type for all the
9798
// v1beta2.HelmRepository (sub)reconcile functions for OCI type. The type implementations
@@ -254,7 +255,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *source
254255
}
255256

256257
func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *sourcev1.HelmRepository) (sreconcile.Result, error) {
257-
var logOpts []registry.LoginOption
258+
var loginOpts []helmreg.LoginOption
258259
// Configure any authentication related options
259260
if obj.Spec.SecretRef != nil {
260261
// Attempt to retrieve secret
@@ -273,7 +274,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
273274
}
274275

275276
// Construct actual options
276-
logOpt, err := loginOptionFromSecret(secret)
277+
loginOpt, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
277278
if err != nil {
278279
e := &serror.Event{
279280
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@@ -284,10 +285,12 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
284285
return sreconcile.ResultEmpty, e
285286
}
286287

287-
logOpts = append(logOpts, logOpt)
288+
if loginOpt != nil {
289+
loginOpts = append(loginOpts, loginOpt)
290+
}
288291
}
289292

290-
if result, err := r.validateSource(ctx, obj, logOpts...); err != nil || result == sreconcile.ResultEmpty {
293+
if result, err := r.validateSource(ctx, obj, loginOpts...); err != nil || result == sreconcile.ResultEmpty {
291294
return result, err
292295
}
293296

@@ -296,7 +299,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
296299

297300
// validateSource the HelmRepository object by checking the url and connecting to the underlying registry
298301
// with he provided credentials.
299-
func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *sourcev1.HelmRepository, logOpts ...registry.LoginOption) (sreconcile.Result, error) {
302+
func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *sourcev1.HelmRepository, logOpts ...helmreg.LoginOption) (sreconcile.Result, error) {
300303
registryClient, file, err := r.RegistryClientGenerator(logOpts != nil)
301304
if err != nil {
302305
e := &serror.Stalling{
@@ -349,14 +352,3 @@ func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *s
349352

350353
return sreconcile.ResultSuccess, nil
351354
}
352-
353-
func loginOptionFromSecret(secret corev1.Secret) (registry.LoginOption, error) {
354-
username, password := string(secret.Data["username"]), string(secret.Data["password"])
355-
switch {
356-
case username == "" && password == "":
357-
return nil, nil
358-
case username == "" || password == "":
359-
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
360-
}
361-
return registry.LoginOptBasicAuth(username, password), nil
362-
}

0 commit comments

Comments
 (0)