Skip to content

Commit b5d26e6

Browse files
committed
Add support for custom certificate and skip-tls-verify in helm OCI
If implemented user will be able to provide their own custom start and bypass tls verification when interacting with OCI registries over https to pull helmCharts. Signed-off-by: Soule BA <[email protected]>
1 parent 38cff76 commit b5d26e6

15 files changed

+612
-196
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: 22 additions & 9 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.Remove(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,26 +993,29 @@ 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
}
1000+
tempFiles := []string{certsTmpDir}
1001+
9901002
getterOpts := clientOpts.GetterOpts
9911003

9921004
var chartRepo repository.Downloader
9931005
if helmreg.IsOCI(normalizedURL) {
994-
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
1006+
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
9951007
if err != nil {
9961008
return nil, fmt.Errorf("failed to create registry client: %w", err)
9971009
}
1010+
tempFiles = append(tempFiles, credentialsFile)
9981011

9991012
var errs []error
10001013
// Tell the chart repository to use the OCI client with the configured getter
10011014
getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
10021015
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
10031016
repository.WithOCIGetterOptions(getterOpts),
10041017
repository.WithOCIRegistryClient(registryClient),
1005-
repository.WithCredentialsFile(credentialsFile))
1018+
repository.WithTempFiles(tempFiles))
10061019
if err != nil {
10071020
errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
10081021
// clean up the credentialsFile
@@ -1016,8 +1029,8 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
10161029

10171030
// If login options are configured, use them to login to the registry
10181031
// The OCIGetter will later retrieve the stored credentials to pull the chart
1019-
if clientOpts.RegLoginOpt != nil {
1020-
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
1032+
if clientOpts.MustLoginToRegistry() {
1033+
err = ociChartRepo.Login(clientOpts.RegLoginOpts...)
10211034
if err != nil {
10221035
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
10231036
// 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)