77 "strings"
88 "time"
99
10+ "github.com/blang/semver/v4"
1011 xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
1112 "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
1213 v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
@@ -30,6 +31,7 @@ const (
3031type compositionObject interface {
3132 SetCompositionUpdatePolicy (* xpv1.UpdatePolicy )
3233 SetCompositionRevisionSelector (* metav1.LabelSelector )
34+ GetCompositionRevisionSelector () * metav1.LabelSelector
3335}
3436
3537// VersionHandler is an interface for handling AppCat versions
@@ -178,7 +180,7 @@ func (vh *DefaultVersionHandler) filterRevisionsByAge(revisions []v1.Composition
178180 } else {
179181 vh .log .Info ("Skipping revision that is too new" ,
180182 "revision" , item .GetName (),
181- "age" , age . Round ( time . Hour ) ,
183+ "age" , age ,
182184 "minimumAge" , minAge )
183185 }
184186 }
@@ -203,6 +205,13 @@ func (vh *DefaultVersionHandler) updateClaim(ctx context.Context, revision strin
203205 if vh .hasAutoUpdateLabel (c ) {
204206 vh .setAutoPolicy (c )
205207 } else {
208+ // Check if target revision is lower than current revision
209+ if shouldSkipUpdate := vh .shouldSkipRevisionUpdate (c , revision ); shouldSkipUpdate {
210+ vh .log .Info ("Skipping revision update: target revision is not higher than current revision" ,
211+ "current" , getCurrentRevisionLabel (c ),
212+ "target" , revision )
213+ return nil
214+ }
206215 vh .applyUpdatePolicy (c , revision )
207216 }
208217 } else {
@@ -231,6 +240,13 @@ func (vh *DefaultVersionHandler) updateComposite(ctx context.Context, revision s
231240 if vh .hasAutoUpdateLabel (comp ) {
232241 vh .setAutoPolicy (comp )
233242 } else {
243+ // Check if target revision is lower than current revision
244+ if shouldSkipUpdate := vh .shouldSkipRevisionUpdate (comp , revision ); shouldSkipUpdate {
245+ vh .log .Info ("Skipping revision update: target revision is not higher than current revision" ,
246+ "current" , getCurrentRevisionLabel (comp ),
247+ "target" , revision )
248+ return nil
249+ }
234250 vh .applyUpdatePolicy (comp , revision )
235251 }
236252 } else {
@@ -293,3 +309,73 @@ func UpdatePolicyPtr(s xpv1.UpdatePolicy) *xpv1.UpdatePolicy {
293309func removeLeadingX (s string ) string {
294310 return strings .TrimLeft (s , "Xx" )
295311}
312+
313+ // parseRevisionLabel extracts and parses the first part of the revision label.
314+ // For example, "v3.60.1-v4.173.1" returns the parsed semver for "v3.60.1".
315+ // Returns an error if the label format is invalid or cannot be parsed as semver.
316+ func parseRevisionLabel (revisionLabel string ) (semver.Version , error ) {
317+ if revisionLabel == "" {
318+ return semver.Version {}, errors .New ("revision label is empty" )
319+ }
320+
321+ // Split by dash and take the first part
322+ parts := strings .Split (revisionLabel , "-" )
323+ firstPart := parts [0 ]
324+
325+ // Parse as semver
326+ version , err := semver .ParseTolerant (firstPart )
327+ if err != nil {
328+ return semver.Version {}, fmt .Errorf ("failed to parse revision label %q: %w" , firstPart , err )
329+ }
330+
331+ return version , nil
332+ }
333+
334+ // getCurrentRevisionLabel extracts the current revision label from a claim or composite.
335+ // Returns an empty string if no revision selector is set (e.g., first deployment, test instance).
336+ func getCurrentRevisionLabel (obj compositionObject ) string {
337+ selector := obj .GetCompositionRevisionSelector ()
338+ if selector == nil || selector .MatchLabels == nil {
339+ return ""
340+ }
341+
342+ return selector .MatchLabels [RevisionLabel ]
343+ }
344+
345+ // shouldSkipRevisionUpdate determines if a revision update should be skipped
346+ // because the target revision is not higher than the current revision.
347+ // Returns false (proceed with update) if:
348+ // - No current revision is set (test deployment)
349+ // - Target revision is higher than current revision
350+ // Returns true (skip update) if:
351+ // - Target revision is lower than or equal to current revision
352+ func (vh * DefaultVersionHandler ) shouldSkipRevisionUpdate (obj compositionObject , targetRevision string ) bool {
353+ currentRevisionLabel := getCurrentRevisionLabel (obj )
354+
355+ // If no current revision is set, then it is a test instance, update
356+ if currentRevisionLabel == "" {
357+ return false
358+ }
359+
360+ // Parse both revisions
361+ currentVersion , err := parseRevisionLabel (currentRevisionLabel )
362+ if err != nil {
363+ vh .log .Error (err , "Failed to parse current revision label, proceeding with update" ,
364+ "currentRevisionLabel" , currentRevisionLabel )
365+ return false
366+ }
367+
368+ targetVersion , err := parseRevisionLabel (targetRevision )
369+ if err != nil {
370+ vh .log .Error (err , "Failed to parse target revision label, proceeding with update" ,
371+ "targetRevision" , targetRevision )
372+ return false
373+ }
374+
375+ // Compare versions: skip update if target <= current
376+ if targetVersion .LTE (currentVersion ) {
377+ return true
378+ }
379+
380+ return false
381+ }
0 commit comments