@@ -21,8 +21,10 @@ import (
21
21
"errors"
22
22
"fmt"
23
23
"os"
24
+ "sort"
24
25
"time"
25
26
27
+ "github.com/Masterminds/semver/v3"
26
28
"github.com/google/go-containerregistry/pkg/crane"
27
29
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
28
30
corev1 "k8s.io/api/core/v1"
@@ -45,6 +47,7 @@ import (
45
47
"github.com/fluxcd/pkg/runtime/patch"
46
48
"github.com/fluxcd/pkg/runtime/predicates"
47
49
"github.com/fluxcd/pkg/untar"
50
+ "github.com/fluxcd/pkg/version"
48
51
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
49
52
serror "github.com/fluxcd/source-controller/internal/error"
50
53
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
@@ -271,78 +274,44 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
271
274
return res , resErr
272
275
}
273
276
274
- // notify emits notification related to the reconciliation.
275
- func (r * OCIRepositoryReconciler ) notify (ctx context.Context , oldObj , newObj * sourcev1.OCIRepository , digest * gcrv1.Hash , res sreconcile.Result , resErr error ) {
276
- // Notify successful reconciliation for new artifact and recovery from any
277
- // failure.
278
- if resErr == nil && res == sreconcile .ResultSuccess && newObj .Status .Artifact != nil {
279
- annotations := map [string ]string {
280
- sourcev1 .GroupVersion .Group + "/revision" : newObj .Status .Artifact .Revision ,
281
- sourcev1 .GroupVersion .Group + "/checksum" : newObj .Status .Artifact .Checksum ,
282
- }
283
-
284
- var oldChecksum string
285
- if oldObj .GetArtifact () != nil {
286
- oldChecksum = oldObj .GetArtifact ().Checksum
287
- }
288
-
289
- message := fmt .Sprintf ("stored artifact with digest '%s' from '%s'" , digest .String (), newObj .Spec .URL )
290
-
291
- // Notify on new artifact and failure recovery.
292
- if oldChecksum != newObj .GetArtifact ().Checksum {
293
- r .AnnotatedEventf (newObj , annotations , corev1 .EventTypeNormal ,
294
- "NewArtifact" , message )
295
- ctrl .LoggerFrom (ctx ).Info (message )
296
- } else {
297
- if sreconcile .FailureRecovery (oldObj , newObj , ociRepositoryFailConditions ) {
298
- r .AnnotatedEventf (newObj , annotations , corev1 .EventTypeNormal ,
299
- meta .SucceededReason , message )
300
- ctrl .LoggerFrom (ctx ).Info (message )
301
- }
302
- }
303
- }
304
- }
305
-
306
- // reconcileSource fetches the upstream OCI artifact content.
277
+ // reconcileSource fetches the upstream OCI artifact metadata and content.
307
278
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
308
279
func (r * OCIRepositoryReconciler ) reconcileSource (ctx context.Context , obj * sourcev1.OCIRepository , digest * gcrv1.Hash , dir string ) (sreconcile.Result , error ) {
309
280
ctxTimeout , cancel := context .WithTimeout (ctx , obj .Spec .Timeout .Duration )
310
281
defer cancel ()
311
282
312
- url := obj .Spec .URL
313
- if obj .Spec .Reference != nil {
314
- if obj .Spec .Reference .Tag != "" {
315
- url = fmt .Sprintf ("%s:%s" , obj .Spec .URL , obj .Spec .Reference .Tag )
316
- }
317
- if obj .Spec .Reference .Digest != "" {
318
- url = fmt .Sprintf ("%s@%s" , obj .Spec .URL , obj .Spec .Reference .Digest )
319
- }
283
+ // Determine which artifact revision to pull
284
+ url , err := r .getArtifactURL (ctxTimeout , obj )
285
+ if err != nil {
286
+ e := & serror.Event {Err : err , Reason : sourcev1 .OCIOperationFailedReason }
287
+ conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Error ())
288
+ return sreconcile .ResultEmpty , e
320
289
}
321
290
322
- // Pull OCI artifact
291
+ // Pull artifact from the remote container registry
323
292
img , err := crane .Pull (url , r .craneOptions (ctxTimeout )... )
324
293
if err != nil {
325
294
e := & serror.Event {Err : err , Reason : sourcev1 .OCIOperationFailedReason }
326
295
conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Error ())
327
296
return sreconcile .ResultEmpty , e
328
297
}
329
298
330
- // Fetch digest
299
+ // Determine the artifact SHA256 digest
331
300
imgDigest , err := img .Digest ()
332
301
if err != nil {
333
302
e := & serror.Event {Err : err , Reason : sourcev1 .OCIOperationFailedReason }
334
303
conditions .MarkTrue (obj , sourcev1 .FetchFailedCondition , e .Reason , e .Error ())
335
304
return sreconcile .ResultEmpty , e
336
305
}
337
306
338
- // Set revision from digest hex
307
+ // Set the internal revision to the remote digest hex
339
308
imgDigest .DeepCopyInto (digest )
340
309
revision := imgDigest .Hex
341
310
342
311
// Mark observations about the revision on the object
343
312
defer func () {
344
313
if ! obj .GetArtifact ().HasRevision (revision ) {
345
- message := fmt .Sprintf ("new upstream revision '%s'" , revision )
314
+ message := fmt .Sprintf ("new upstream revision '%s' for '%s' " , revision , url )
346
315
conditions .MarkTrue (obj , sourcev1 .ArtifactOutdatedCondition , "NewRevision" , message )
347
316
conditions .MarkReconciling (obj , "NewRevision" , message )
348
317
}
@@ -382,6 +351,76 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
382
351
return sreconcile .ResultSuccess , nil
383
352
}
384
353
354
+ // getArtifactURL determines which tag or digest should be used and returns the OCI artifact FQN.
355
+ func (r * OCIRepositoryReconciler ) getArtifactURL (ctx context.Context , obj * sourcev1.OCIRepository ) (string , error ) {
356
+ url := obj .Spec .URL
357
+ if obj .Spec .Reference != nil {
358
+ if obj .Spec .Reference .Digest != "" {
359
+ return fmt .Sprintf ("%s@%s" , obj .Spec .URL , obj .Spec .Reference .Digest ), nil
360
+ }
361
+
362
+ if obj .Spec .Reference .SemVer != "" {
363
+ tag , err := r .getTagBySemver (ctx , url , obj .Spec .Reference .SemVer )
364
+ if err != nil {
365
+ return "" , err
366
+ }
367
+ return fmt .Sprintf ("%s:%s" , obj .Spec .URL , tag ), nil
368
+ }
369
+
370
+ if obj .Spec .Reference .Tag != "latest" {
371
+ return fmt .Sprintf ("%s:%s" , obj .Spec .URL , obj .Spec .Reference .Tag ), nil
372
+ }
373
+ }
374
+
375
+ return url , nil
376
+ }
377
+
378
+ // getTagBySemver call the remote container registry, fetches all the tags from the repository,
379
+ // and returns the latest tag according to the semver expression.
380
+ func (r * OCIRepositoryReconciler ) getTagBySemver (ctx context.Context , url , exp string ) (string , error ) {
381
+ tags , err := crane .ListTags (url , r .craneOptions (ctx )... )
382
+ if err != nil {
383
+ return "" , err
384
+ }
385
+
386
+ constraint , err := semver .NewConstraint (exp )
387
+ if err != nil {
388
+ return "" , fmt .Errorf ("semver '%s' parse error: %w" , exp , err )
389
+ }
390
+
391
+ var matchingVersions []* semver.Version
392
+ for _ , t := range tags {
393
+ v , err := version .ParseVersion (t )
394
+ if err != nil {
395
+ continue
396
+ }
397
+
398
+ if constraint .Check (v ) {
399
+ matchingVersions = append (matchingVersions , v )
400
+ }
401
+ }
402
+
403
+ if len (matchingVersions ) == 0 {
404
+ return "" , fmt .Errorf ("no match found for semver: %s" , exp )
405
+ }
406
+
407
+ sort .Sort (sort .Reverse (semver .Collection (matchingVersions )))
408
+ return matchingVersions [0 ].Original (), nil
409
+ }
410
+
411
+ // craneOptions sets the timeout and user agent for all operations against remote container registries.
412
+ func (r * OCIRepositoryReconciler ) craneOptions (ctx context.Context ) []crane.Option {
413
+ return []crane.Option {
414
+ crane .WithContext (ctx ),
415
+ crane .WithUserAgent ("flux/v2" ),
416
+ crane .WithPlatform (& gcrv1.Platform {
417
+ Architecture : "flux" ,
418
+ OS : "flux" ,
419
+ OSVersion : "v2" ,
420
+ }),
421
+ }
422
+ }
423
+
385
424
// reconcileStorage ensures the current state of the storage matches the
386
425
// desired and previously observed state.
387
426
//
@@ -580,14 +619,34 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context, obj runtime.Obj
580
619
r .Eventf (obj , eventType , reason , msg )
581
620
}
582
621
583
- func (r * OCIRepositoryReconciler ) craneOptions (ctx context.Context ) []crane.Option {
584
- return []crane.Option {
585
- crane .WithContext (ctx ),
586
- crane .WithUserAgent ("flux/v2" ),
587
- crane .WithPlatform (& gcrv1.Platform {
588
- Architecture : "flux" ,
589
- OS : "flux" ,
590
- OSVersion : "v2" ,
591
- }),
622
+ // notify emits notification related to the reconciliation.
623
+ func (r * OCIRepositoryReconciler ) notify (ctx context.Context , oldObj , newObj * sourcev1.OCIRepository , digest * gcrv1.Hash , res sreconcile.Result , resErr error ) {
624
+ // Notify successful reconciliation for new artifact and recovery from any
625
+ // failure.
626
+ if resErr == nil && res == sreconcile .ResultSuccess && newObj .Status .Artifact != nil {
627
+ annotations := map [string ]string {
628
+ sourcev1 .GroupVersion .Group + "/revision" : newObj .Status .Artifact .Revision ,
629
+ sourcev1 .GroupVersion .Group + "/checksum" : newObj .Status .Artifact .Checksum ,
630
+ }
631
+
632
+ var oldChecksum string
633
+ if oldObj .GetArtifact () != nil {
634
+ oldChecksum = oldObj .GetArtifact ().Checksum
635
+ }
636
+
637
+ message := fmt .Sprintf ("stored artifact with digest '%s' from '%s'" , digest .String (), newObj .Spec .URL )
638
+
639
+ // Notify on new artifact and failure recovery.
640
+ if oldChecksum != newObj .GetArtifact ().Checksum {
641
+ r .AnnotatedEventf (newObj , annotations , corev1 .EventTypeNormal ,
642
+ "NewArtifact" , message )
643
+ ctrl .LoggerFrom (ctx ).Info (message )
644
+ } else {
645
+ if sreconcile .FailureRecovery (oldObj , newObj , ociRepositoryFailConditions ) {
646
+ r .AnnotatedEventf (newObj , annotations , corev1 .EventTypeNormal ,
647
+ meta .SucceededReason , message )
648
+ ctrl .LoggerFrom (ctx ).Info (message )
649
+ }
650
+ }
592
651
}
593
652
}
0 commit comments