Skip to content

Commit 7f6e65c

Browse files
Merge pull request #535 from vshn/disallow-rollback-main
Block maintenance from rolling back
2 parents 15ec97a + dcf5b47 commit 7f6e65c

File tree

2 files changed

+459
-1
lines changed

2 files changed

+459
-1
lines changed

pkg/maintenance/release/release.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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 (
3031
type 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 {
293309
func 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

Comments
 (0)