@@ -29,6 +29,7 @@ import (
29
29
"time"
30
30
31
31
helmgetter "helm.sh/helm/v3/pkg/getter"
32
+ "helm.sh/helm/v3/pkg/registry"
32
33
corev1 "k8s.io/api/core/v1"
33
34
apierrs "k8s.io/apimachinery/pkg/api/errors"
34
35
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -116,9 +117,10 @@ type HelmChartReconciler struct {
116
117
kuberecorder.EventRecorder
117
118
helper.Metrics
118
119
119
- Storage * Storage
120
- Getters helmgetter.Providers
121
- ControllerName string
120
+ RegistryClientGenerator RegistryClientGeneratorFunc
121
+ Storage * Storage
122
+ Getters helmgetter.Providers
123
+ ControllerName string
122
124
123
125
Cache * cache.Cache
124
126
TTL time.Duration
@@ -378,15 +380,19 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
378
380
379
381
// Assert source has an artifact
380
382
if s .GetArtifact () == nil || ! r .Storage .ArtifactExist (* s .GetArtifact ()) {
381
- conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , "NoSourceArtifact" ,
382
- "no artifact available for %s source '%s'" , obj .Spec .SourceRef .Kind , obj .Spec .SourceRef .Name )
383
- r .eventLogf (ctx , obj , events .EventTypeTrace , "NoSourceArtifact" ,
384
- "no artifact available for %s source '%s'" , obj .Spec .SourceRef .Kind , obj .Spec .SourceRef .Name )
385
- return sreconcile .ResultRequeue , nil
383
+ if helmRepo , ok := s .(* sourcev1.HelmRepository ); ! ok || ! registry .IsOCI (helmRepo .Spec .URL ) {
384
+ conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , "NoSourceArtifact" ,
385
+ "no artifact available for %s source '%s'" , obj .Spec .SourceRef .Kind , obj .Spec .SourceRef .Name )
386
+ r .eventLogf (ctx , obj , events .EventTypeTrace , "NoSourceArtifact" ,
387
+ "no artifact available for %s source '%s'" , obj .Spec .SourceRef .Kind , obj .Spec .SourceRef .Name )
388
+ return sreconcile .ResultRequeue , nil
389
+ }
386
390
}
387
391
388
- // Record current artifact revision as last observed
389
- obj .Status .ObservedSourceArtifactRevision = s .GetArtifact ().Revision
392
+ if s .GetArtifact () != nil {
393
+ // Record current artifact revision as last observed
394
+ obj .Status .ObservedSourceArtifactRevision = s .GetArtifact ().Revision
395
+ }
390
396
391
397
// Defer observation of build result
392
398
defer func () {
@@ -439,7 +445,10 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
439
445
// object, and returns early.
440
446
func (r * HelmChartReconciler ) buildFromHelmRepository (ctx context.Context , obj * sourcev1.HelmChart ,
441
447
repo * sourcev1.HelmRepository , b * chart.Build ) (sreconcile.Result , error ) {
442
- var tlsConfig * tls.Config
448
+ var (
449
+ tlsConfig * tls.Config
450
+ logOpts []registry.LoginOption
451
+ )
443
452
444
453
// Construct the Getter options from the HelmRepository data
445
454
clientOpts := []helmgetter.Option {
@@ -481,32 +490,93 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
481
490
// Requeue as content of secret might change
482
491
return sreconcile .ResultEmpty , e
483
492
}
484
- }
485
493
486
- // Initialize the chart repository
487
- chartRepo , err := repository .NewChartRepository (repo .Spec .URL , r .Storage .LocalPath (* repo .GetArtifact ()), r .Getters , tlsConfig , clientOpts ,
488
- repository .WithMemoryCache (r .Storage .LocalPath (* repo .GetArtifact ()), r .Cache , r .TTL , func (event string ) {
489
- r .IncCacheEvents (event , obj .Name , obj .Namespace )
490
- }))
491
- if err != nil {
492
- // Any error requires a change in generation,
493
- // which we should be informed about by the watcher
494
- switch err .(type ) {
495
- case * url.Error :
496
- e := & serror.Stalling {
497
- Err : fmt .Errorf ("invalid Helm repository URL: %w" , err ),
498
- Reason : sourcev1 .URLInvalidReason ,
494
+ // Build registryClient options from secret
495
+ logOpt , err := loginOptionFromSecret (* secret )
496
+ if err != nil {
497
+ e := & serror.Event {
498
+ Err : fmt .Errorf ("failed to configure Helm client with secret data: %w" , err ),
499
+ Reason : sourcev1 .AuthenticationFailedReason ,
499
500
}
500
501
conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Err .Error ())
502
+ // Requeue as content of secret might change
501
503
return sreconcile .ResultEmpty , e
502
- default :
503
- e := & serror.Stalling {
504
- Err : fmt .Errorf ("failed to construct Helm client: %w" , err ),
505
- Reason : meta .FailedReason ,
504
+ }
505
+
506
+ logOpts = append ([]registry.LoginOption {}, logOpt )
507
+ }
508
+
509
+ // Initialize the chart repository
510
+ var chartRepo chart.Remote
511
+ switch repo .Spec .Type {
512
+ case sourcev1 .HelmRepositoryTypeOCI :
513
+ if ! registry .IsOCI (repo .Spec .URL ) {
514
+ err := fmt .Errorf ("invalid OCI registry URL: %s" , repo .Spec .URL )
515
+ return chartRepoErrorReturn (err , obj )
516
+ }
517
+
518
+ // with this function call, we create a temporary file to store the credentials if needed.
519
+ // this is needed because otherwise the credentials are stored in ~/.docker/config.json.
520
+ // TODO@souleb: remove this once the registry move to Oras v2
521
+ // or rework to enable reusing credentials to avoid the unneccessary handshake operations
522
+ registryClient , file , err := r .RegistryClientGenerator (logOpts != nil )
523
+ if err != nil {
524
+ return chartRepoErrorReturn (err , obj )
525
+ }
526
+
527
+ if file != "" {
528
+ defer func () {
529
+ os .Remove (file )
530
+ }()
531
+ }
532
+
533
+ // Tell the chart repository to use the OCI client with the configured getter
534
+ clientOpts = append (clientOpts , helmgetter .WithRegistryClient (registryClient ))
535
+ ociChartRepo , err := repository .NewOCIChartRepository (repo .Spec .URL , repository .WithOCIGetter (r .Getters ), repository .WithOCIGetterOptions (clientOpts ), repository .WithOCIRegistryClient (registryClient ))
536
+ if err != nil {
537
+ return chartRepoErrorReturn (err , obj )
538
+ }
539
+ chartRepo = ociChartRepo
540
+
541
+ // If login options are configured, use them to login to the registry
542
+ // The OCIGetter will later retrieve the stored credentials to pull the chart
543
+ if logOpts != nil {
544
+ err = ociChartRepo .Login (logOpts ... )
545
+ if err != nil {
546
+ return chartRepoErrorReturn (err , obj )
506
547
}
507
- conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Err .Error ())
508
- return sreconcile .ResultEmpty , e
509
548
}
549
+ default :
550
+ var httpChartRepo * repository.ChartRepository
551
+ httpChartRepo , err := repository .NewChartRepository (repo .Spec .URL , r .Storage .LocalPath (* repo .GetArtifact ()), r .Getters , tlsConfig , clientOpts ,
552
+ repository .WithMemoryCache (r .Storage .LocalPath (* repo .GetArtifact ()), r .Cache , r .TTL , func (event string ) {
553
+ r .IncCacheEvents (event , obj .Name , obj .Namespace )
554
+ }))
555
+ if err != nil {
556
+ return chartRepoErrorReturn (err , obj )
557
+ }
558
+ chartRepo = httpChartRepo
559
+ defer func () {
560
+ if httpChartRepo == nil {
561
+ return
562
+ }
563
+ // Cache the index if it was successfully retrieved
564
+ // and the chart was successfully built
565
+ if r .Cache != nil && httpChartRepo .Index != nil {
566
+ // The cache key have to be safe in multi-tenancy environments,
567
+ // as otherwise it could be used as a vector to bypass the helm repository's authentication.
568
+ // Using r.Storage.LocalPath(*repo.GetArtifact() is safe as the path is in the format /<helm-repository-name>/<chart-name>/<filename>.
569
+ err := httpChartRepo .CacheIndexInMemory ()
570
+ if err != nil {
571
+ r .eventLogf (ctx , obj , events .EventTypeTrace , sourcev1 .CacheOperationFailedReason , "failed to cache index: %s" , err )
572
+ }
573
+ }
574
+
575
+ // Delete the index reference
576
+ if httpChartRepo .Index != nil {
577
+ httpChartRepo .Unload ()
578
+ }
579
+ }()
510
580
}
511
581
512
582
// Construct the chart builder with scoped configuration
@@ -532,25 +602,6 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
532
602
return sreconcile .ResultEmpty , err
533
603
}
534
604
535
- defer func () {
536
- // Cache the index if it was successfully retrieved
537
- // and the chart was successfully built
538
- if r .Cache != nil && chartRepo .Index != nil {
539
- // The cache key have to be safe in multi-tenancy environments,
540
- // as otherwise it could be used as a vector to bypass the helm repository's authentication.
541
- // Using r.Storage.LocalPath(*repo.GetArtifact() is safe as the path is in the format /<helm-repository-name>/<chart-name>/<filename>.
542
- err := chartRepo .CacheIndexInMemory ()
543
- if err != nil {
544
- r .eventLogf (ctx , obj , events .EventTypeTrace , sourcev1 .CacheOperationFailedReason , "failed to cache index: %s" , err )
545
- }
546
- }
547
-
548
- // Delete the index reference
549
- if chartRepo .Index != nil {
550
- chartRepo .Unload ()
551
- }
552
- }()
553
-
554
605
* b = * build
555
606
return sreconcile .ResultSuccess , nil
556
607
}
@@ -1090,3 +1141,22 @@ func reasonForBuild(build *chart.Build) string {
1090
1141
}
1091
1142
return sourcev1 .ChartPullSucceededReason
1092
1143
}
1144
+
1145
+ func chartRepoErrorReturn (err error , obj * sourcev1.HelmChart ) (sreconcile.Result , error ) {
1146
+ switch err .(type ) {
1147
+ case * url.Error :
1148
+ e := & serror.Stalling {
1149
+ Err : fmt .Errorf ("invalid Helm repository URL: %w" , err ),
1150
+ Reason : sourcev1 .URLInvalidReason ,
1151
+ }
1152
+ conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Err .Error ())
1153
+ return sreconcile .ResultEmpty , e
1154
+ default :
1155
+ e := & serror.Stalling {
1156
+ Err : fmt .Errorf ("failed to construct Helm client: %w" , err ),
1157
+ Reason : meta .FailedReason ,
1158
+ }
1159
+ conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Err .Error ())
1160
+ return sreconcile .ResultEmpty , e
1161
+ }
1162
+ }
0 commit comments