Skip to content

Commit 6fa8d05

Browse files
authored
Merge pull request #1097 from souleb/custom-cert-oci
Helm OCI: Add support for TLS registries with self-signed certs
2 parents 6377c6f + d45c08c commit 6fa8d05

14 files changed

+612
-182
lines changed

docs/spec/v1beta2/helmrepositories.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,8 +459,6 @@ a deprecation warning will be logged.
459459

460460
### Cert secret reference
461461

462-
**Note:** TLS authentication is not yet supported by OCI Helm repositories.
463-
464462
`.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
465463
certificate data. The secret can contain the following keys:
466464

internal/controller/helmchart_controller.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
512512
if err != nil {
513513
return chartRepoConfigErrorReturn(err, obj)
514514
}
515-
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
515+
516+
clientOpts, certsTmpDir, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
516517
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
517518
e := &serror.Event{
518519
Err: err,
@@ -521,6 +522,15 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
521522
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
522523
return sreconcile.ResultEmpty, e
523524
}
525+
if certsTmpDir != "" {
526+
defer func() {
527+
if err := os.RemoveAll(certsTmpDir); err != nil {
528+
r.eventLogf(ctx, obj, corev1.EventTypeWarning, meta.FailedReason,
529+
"failed to delete temporary certificates directory: %s", err)
530+
}
531+
}()
532+
}
533+
524534
getterOpts := clientOpts.GetterOpts
525535

526536
// Initialize the chart repository
@@ -536,7 +546,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
536546
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
537547
// TODO@souleb: remove this once the registry move to Oras v2
538548
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
539-
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
549+
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
540550
if err != nil {
541551
e := &serror.Event{
542552
Err: fmt.Errorf("failed to construct Helm client: %w", err),
@@ -585,8 +595,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
585595

586596
// If login options are configured, use them to login to the registry
587597
// The OCIGetter will later retrieve the stored credentials to pull the chart
588-
if clientOpts.RegLoginOpt != nil {
589-
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
598+
if clientOpts.MustLoginToRegistry() {
599+
err = ociChartRepo.Login(clientOpts.RegLoginOpts...)
590600
if err != nil {
591601
e := &serror.Event{
592602
Err: fmt.Errorf("failed to login to OCI registry: %w", err),
@@ -983,15 +993,15 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
983993
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
984994
defer cancel()
985995

986-
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
996+
clientOpts, certsTmpDir, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
987997
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
988998
return nil, err
989999
}
9901000
getterOpts := clientOpts.GetterOpts
9911001

9921002
var chartRepo repository.Downloader
9931003
if helmreg.IsOCI(normalizedURL) {
994-
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
1004+
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
9951005
if err != nil {
9961006
return nil, fmt.Errorf("failed to create registry client: %w", err)
9971007
}
@@ -1002,6 +1012,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
10021012
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
10031013
repository.WithOCIGetterOptions(getterOpts),
10041014
repository.WithOCIRegistryClient(registryClient),
1015+
repository.WithCertificatesStore(certsTmpDir),
10051016
repository.WithCredentialsFile(credentialsFile))
10061017
if err != nil {
10071018
errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
@@ -1016,8 +1027,8 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
10161027

10171028
// If login options are configured, use them to login to the registry
10181029
// The OCIGetter will later retrieve the stored credentials to pull the chart
1019-
if clientOpts.RegLoginOpt != nil {
1020-
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
1030+
if clientOpts.MustLoginToRegistry() {
1031+
err = ociChartRepo.Login(clientOpts.RegLoginOpts...)
10211032
if err != nil {
10221033
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
10231034
// clean up the credentialsFile

internal/controller/helmchart_controller_test.go

Lines changed: 125 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
11091109
g.Expect(err).NotTo(HaveOccurred())
11101110

11111111
// Upload the test chart
1112-
metadata, err := loadTestChartToOCI(chartData, chartPath, testRegistryServer)
1112+
metadata, err := loadTestChartToOCI(chartData, testRegistryServer, "", "", "")
11131113
g.Expect(err).NotTo(HaveOccurred())
11141114

11151115
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
@@ -2244,53 +2244,74 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
22442244
url string
22452245
registryOpts registryOptions
22462246
secretOpts secretOptions
2247+
secret *corev1.Secret
2248+
certsecret *corev1.Secret
2249+
insecure bool
22472250
provider string
22482251
providerImg string
22492252
want sreconcile.Result
22502253
wantErr bool
22512254
assertConditions []metav1.Condition
22522255
}{
22532256
{
2254-
name: "HTTP without basic auth",
2255-
want: sreconcile.ResultSuccess,
2257+
name: "HTTP without basic auth",
2258+
want: sreconcile.ResultSuccess,
2259+
insecure: true,
22562260
assertConditions: []metav1.Condition{
22572261
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
22582262
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
22592263
},
22602264
},
22612265
{
2262-
name: "HTTP with basic auth secret",
2263-
want: sreconcile.ResultSuccess,
2266+
name: "HTTP with basic auth secret",
2267+
want: sreconcile.ResultSuccess,
2268+
insecure: true,
22642269
registryOpts: registryOptions{
22652270
withBasicAuth: true,
22662271
},
22672272
secretOpts: secretOptions{
22682273
username: testRegistryUsername,
22692274
password: testRegistryPassword,
22702275
},
2276+
secret: &corev1.Secret{
2277+
ObjectMeta: metav1.ObjectMeta{
2278+
Name: "auth-secretref",
2279+
},
2280+
Type: corev1.SecretTypeDockerConfigJson,
2281+
Data: map[string][]byte{},
2282+
},
22712283
assertConditions: []metav1.Condition{
22722284
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
22732285
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
22742286
},
22752287
},
22762288
{
2277-
name: "HTTP registry - basic auth with invalid secret",
2278-
want: sreconcile.ResultEmpty,
2279-
wantErr: true,
2289+
name: "HTTP registry - basic auth with invalid secret",
2290+
want: sreconcile.ResultEmpty,
2291+
wantErr: true,
2292+
insecure: true,
22802293
registryOpts: registryOptions{
22812294
withBasicAuth: true,
22822295
},
22832296
secretOpts: secretOptions{
22842297
username: "wrong-pass",
22852298
password: "wrong-pass",
22862299
},
2300+
secret: &corev1.Secret{
2301+
ObjectMeta: metav1.ObjectMeta{
2302+
Name: "auth-secretref",
2303+
},
2304+
Type: corev1.SecretTypeDockerConfigJson,
2305+
Data: map[string][]byte{},
2306+
},
22872307
assertConditions: []metav1.Condition{
22882308
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to login to OCI registry"),
22892309
},
22902310
},
22912311
{
22922312
name: "with contextual login provider",
22932313
wantErr: true,
2314+
insecure: true,
22942315
provider: "aws",
22952316
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
22962317
assertConditions: []metav1.Condition{
@@ -2303,16 +2324,87 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
23032324
registryOpts: registryOptions{
23042325
withBasicAuth: true,
23052326
},
2327+
insecure: true,
23062328
secretOpts: secretOptions{
23072329
username: testRegistryUsername,
23082330
password: testRegistryPassword,
23092331
},
2332+
secret: &corev1.Secret{
2333+
ObjectMeta: metav1.ObjectMeta{
2334+
Name: "auth-secretref",
2335+
},
2336+
Type: corev1.SecretTypeDockerConfigJson,
2337+
Data: map[string][]byte{},
2338+
},
23102339
provider: "azure",
23112340
assertConditions: []metav1.Condition{
23122341
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
23132342
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
23142343
},
23152344
},
2345+
{
2346+
name: "HTTPS With invalid CA cert",
2347+
wantErr: true,
2348+
registryOpts: registryOptions{
2349+
withTLS: true,
2350+
withClientCertAuth: true,
2351+
},
2352+
secretOpts: secretOptions{
2353+
username: testRegistryUsername,
2354+
password: testRegistryPassword,
2355+
},
2356+
secret: &corev1.Secret{
2357+
ObjectMeta: metav1.ObjectMeta{
2358+
Name: "auth-secretref",
2359+
},
2360+
Type: corev1.SecretTypeDockerConfigJson,
2361+
Data: map[string][]byte{},
2362+
},
2363+
certsecret: &corev1.Secret{
2364+
ObjectMeta: metav1.ObjectMeta{
2365+
Name: "certs-secretref",
2366+
},
2367+
Data: map[string][]byte{
2368+
"caFile": []byte("invalid caFile"),
2369+
},
2370+
},
2371+
assertConditions: []metav1.Condition{
2372+
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to construct Helm client's TLS config: cannot append certificate into certificate pool: invalid caFile"),
2373+
},
2374+
},
2375+
{
2376+
name: "HTTPS With CA cert",
2377+
want: sreconcile.ResultSuccess,
2378+
registryOpts: registryOptions{
2379+
withTLS: true,
2380+
withClientCertAuth: true,
2381+
},
2382+
secretOpts: secretOptions{
2383+
username: testRegistryUsername,
2384+
password: testRegistryPassword,
2385+
},
2386+
secret: &corev1.Secret{
2387+
ObjectMeta: metav1.ObjectMeta{
2388+
Name: "auth-secretref",
2389+
},
2390+
Type: corev1.SecretTypeDockerConfigJson,
2391+
Data: map[string][]byte{},
2392+
},
2393+
certsecret: &corev1.Secret{
2394+
ObjectMeta: metav1.ObjectMeta{
2395+
Name: "certs-secretref",
2396+
},
2397+
Data: map[string][]byte{
2398+
"caFile": tlsCA,
2399+
"certFile": clientPublicKey,
2400+
"keyFile": clientPrivateKey,
2401+
},
2402+
},
2403+
assertConditions: []metav1.Condition{
2404+
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
2405+
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
2406+
},
2407+
},
23162408
}
23172409

23182410
for _, tt := range tests {
@@ -2325,7 +2417,9 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
23252417

23262418
workspaceDir := t.TempDir()
23272419

2328-
tt.registryOpts.disableDNSMocking = true
2420+
if tt.insecure {
2421+
tt.registryOpts.disableDNSMocking = true
2422+
}
23292423
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
23302424
g.Expect(err).NotTo(HaveOccurred())
23312425
t.Cleanup(func() {
@@ -2337,7 +2431,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
23372431
g.Expect(err).ToNot(HaveOccurred())
23382432

23392433
// Upload the test chart
2340-
metadata, err := loadTestChartToOCI(chartData, chartPath, server)
2434+
metadata, err := loadTestChartToOCI(chartData, server, "testdata/certs/client.pem", "testdata/certs/client-key.pem", "testdata/certs/ca.pem")
23412435
g.Expect(err).ToNot(HaveOccurred())
23422436

23432437
repo := &helmv1.HelmRepository{
@@ -2364,25 +2458,26 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
23642458
}
23652459

23662460
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
2367-
secret := &corev1.Secret{
2368-
ObjectMeta: metav1.ObjectMeta{
2369-
Name: "auth-secretref",
2370-
},
2371-
Type: corev1.SecretTypeDockerConfigJson,
2372-
Data: map[string][]byte{
2373-
".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
2374-
server.registryHost, tt.secretOpts.username, tt.secretOpts.password)),
2375-
},
2376-
}
2461+
tt.secret.Data[".dockerconfigjson"] = []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
2462+
server.registryHost, tt.secretOpts.username, tt.secretOpts.password))
2463+
}
23772464

2465+
if tt.secret != nil {
23782466
repo.Spec.SecretRef = &meta.LocalObjectReference{
2379-
Name: secret.Name,
2467+
Name: tt.secret.Name,
23802468
}
2381-
clientBuilder.WithObjects(secret, repo)
2382-
} else {
2383-
clientBuilder.WithObjects(repo)
2469+
clientBuilder.WithObjects(tt.secret)
2470+
}
2471+
2472+
if tt.certsecret != nil {
2473+
repo.Spec.CertSecretRef = &meta.LocalObjectReference{
2474+
Name: tt.certsecret.Name,
2475+
}
2476+
clientBuilder.WithObjects(tt.certsecret)
23842477
}
23852478

2479+
clientBuilder.WithObjects(repo)
2480+
23862481
obj := &helmv1.HelmChart{
23872482
ObjectMeta: metav1.ObjectMeta{
23882483
GenerateName: "auth-strategy-",
@@ -2456,7 +2551,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T
24562551
g.Expect(err).ToNot(HaveOccurred())
24572552

24582553
// Upload the test chart
2459-
metadata, err := loadTestChartToOCI(chartData, chartPath, server)
2554+
metadata, err := loadTestChartToOCI(chartData, server, "", "", "")
24602555
g.Expect(err).NotTo(HaveOccurred())
24612556

24622557
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
@@ -2687,30 +2782,24 @@ func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
26872782
return ch.Metadata, nil
26882783
}
26892784

2690-
func loadTestChartToOCI(chartData []byte, chartPath string, server *registryClientTestServer) (*hchart.Metadata, error) {
2785+
func loadTestChartToOCI(chartData []byte, server *registryClientTestServer, certFile, keyFile, cafile string) (*hchart.Metadata, error) {
26912786
// Login to the registry
26922787
err := server.registryClient.Login(server.registryHost,
26932788
helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword),
2694-
helmreg.LoginOptInsecure(true))
2695-
if err != nil {
2696-
return nil, err
2697-
}
2698-
2699-
// Load a test chart
2700-
chartData, err = os.ReadFile(chartPath)
2789+
helmreg.LoginOptTLSClientConfig(certFile, keyFile, cafile))
27012790
if err != nil {
2702-
return nil, err
2791+
return nil, fmt.Errorf("failed to login to OCI registry: %w", err)
27032792
}
27042793
metadata, err := extractChartMeta(chartData)
27052794
if err != nil {
2706-
return nil, err
2795+
return nil, fmt.Errorf("failed to extract chart metadata: %w", err)
27072796
}
27082797

27092798
// Upload the test chart
27102799
ref := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
27112800
_, err = server.registryClient.Push(chartData, ref)
27122801
if err != nil {
2713-
return nil, err
2802+
return nil, fmt.Errorf("failed to push chart: %w", err)
27142803
}
27152804

27162805
return metadata, nil

internal/controller/helmrepository_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
399399
return sreconcile.ResultEmpty, e
400400
}
401401

402-
clientOpts, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
402+
clientOpts, _, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
403403
if err != nil {
404404
if errors.Is(err, getter.ErrDeprecatedTLSConfig) {
405405
ctrl.LoggerFrom(ctx).

0 commit comments

Comments
 (0)