Skip to content

Commit a2e5062

Browse files
explicitly read pkg.Release, falling back to build metadata parsing when comparison returns equal
Signed-off-by: Rashmi Gottipati <rgottipa@redhat.com>
1 parent 8dcab49 commit a2e5062

File tree

5 files changed

+109
-24
lines changed

5 files changed

+109
-24
lines changed

internal/operator-controller/catalogmetadata/compare/compare.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/operator-framework/operator-registry/alpha/declcfg"
1111

1212
"github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil"
13+
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
1314
slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices"
1415
)
1516

@@ -39,9 +40,22 @@ func newMastermindsRange(versionRange string) (bsemver.Range, error) {
3940
}
4041

4142
// ByVersionAndRelease is a comparison function that compares bundles by
42-
// version and release. Bundles with lower versions/releases are
43-
// considered less than bundles with higher versions/releases.
43+
// version and release. When the CompositeVersionComparison feature gate is
44+
// enabled, it uses Bundle.Compare() (which reads pkg.Release from olm.package)
45+
// and falls back to build metadata comparison if equal. When disabled, it uses
46+
// version+release from build metadata (backward compatible).
4447
func ByVersionAndRelease(b1, b2 declcfg.Bundle) int {
48+
if features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison) {
49+
// Use CompositeVersion comparison (reads pkg.Release from olm.package)
50+
result := b2.Compare(&b1)
51+
if result != 0 {
52+
return result
53+
}
54+
// If CompositeVersion comparison is equal, fall back to build metadata
55+
return compareByBuildMetadata(b1, b2)
56+
}
57+
58+
// Default: use version+release from build metadata (backward compatible)
4559
vr1, err1 := bundleutil.GetVersionAndRelease(b1)
4660
vr2, err2 := bundleutil.GetVersionAndRelease(b2)
4761

@@ -54,6 +68,18 @@ func ByVersionAndRelease(b1, b2 declcfg.Bundle) int {
5468
return vr2.Compare(*vr1)
5569
}
5670

71+
// compareByBuildMetadata compares bundles using build metadata parsing.
72+
// This is used as a fallback when CompositeVersion comparison returns equal.
73+
func compareByBuildMetadata(b1, b2 declcfg.Bundle) int {
74+
vr1, err1 := bundleutil.GetVersionAndRelease(b1)
75+
vr2, err2 := bundleutil.GetVersionAndRelease(b2)
76+
77+
if err1 != nil || err2 != nil {
78+
return compareErrors(err2, err1)
79+
}
80+
return vr2.Compare(*vr1)
81+
}
82+
5783
func ByDeprecationFunc(deprecation declcfg.Deprecation) func(a, b declcfg.Bundle) int {
5884
deprecatedBundles := sets.New[string]()
5985
for _, entry := range deprecation.Entries {

internal/operator-controller/catalogmetadata/compare/compare_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package compare_test
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"slices"
67
"testing"
78

@@ -13,6 +14,7 @@ import (
1314
"github.com/operator-framework/operator-registry/alpha/property"
1415

1516
"github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/compare"
17+
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
1618
)
1719

1820
func TestNewVersionRange(t *testing.T) {
@@ -168,3 +170,59 @@ func TestByDeprecationFunc(t *testing.T) {
168170
assert.Equal(t, 0, byDeprecation(c, d))
169171
assert.Equal(t, 0, byDeprecation(d, c))
170172
}
173+
174+
// TestByVersionAndRelease_WithCompositeVersionComparison tests the feature-gated hybrid comparison:
175+
// When disabled: uses build metadata (backward compatible)
176+
// When enabled: uses Bundle.Compare() with build metadata fallback for registry+v1
177+
func TestByVersionAndRelease_WithCompositeVersionComparison(t *testing.T) {
178+
// Registry+v1 bundles: same version, different build metadata
179+
registryV1_b1 := declcfg.Bundle{
180+
Name: "package1.v1.0.0+1",
181+
Properties: []property.Property{
182+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0+1"}`)},
183+
},
184+
}
185+
registryV1_b2 := declcfg.Bundle{
186+
Name: "package1.v1.0.0+2",
187+
Properties: []property.Property{
188+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0+2"}`)},
189+
},
190+
}
191+
192+
// New format bundles: same version, different .spec.release
193+
newFormat_b1 := declcfg.Bundle{
194+
Name: "package2.v1.0.0-rel.1",
195+
Properties: []property.Property{
196+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package2", "version": "1.0.0", "release": "1"}`)},
197+
},
198+
}
199+
newFormat_b2 := declcfg.Bundle{
200+
Name: "package2.v1.0.0-rel.2",
201+
Properties: []property.Property{
202+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package2", "version": "1.0.0", "release": "2"}`)},
203+
},
204+
}
205+
206+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison)
207+
t.Cleanup(func() {
208+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.CompositeVersionComparison, prevEnabled)))
209+
})
210+
211+
t.Run("feature gate disabled - uses build metadata", func(t *testing.T) {
212+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.CompositeVersionComparison)))
213+
result := compare.ByVersionAndRelease(registryV1_b1, registryV1_b2)
214+
assert.Greater(t, result, 0, "should sort by build metadata: 1.0.0+2 > 1.0.0+1")
215+
})
216+
217+
t.Run("feature gate enabled - registry+v1 bundles use build metadata fallback", func(t *testing.T) {
218+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.CompositeVersionComparison)))
219+
result := compare.ByVersionAndRelease(registryV1_b1, registryV1_b2)
220+
assert.Greater(t, result, 0, "should fallback to build metadata: 1.0.0+2 > 1.0.0+1")
221+
})
222+
223+
t.Run("feature gate enabled - new format bundles use .spec.release", func(t *testing.T) {
224+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.CompositeVersionComparison)))
225+
result := compare.ByVersionAndRelease(newFormat_b1, newFormat_b2)
226+
assert.Greater(t, result, 0, "should use .spec.release: release=2 > release=1")
227+
})
228+
}

internal/operator-controller/catalogmetadata/filter/successors.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Chann
3535
ExactVersionRelease(*installedVersionRelease),
3636
}
3737

38-
// If release version priority is enabled, also consider bundles
38+
// If CompositeVersionComparison is enabled, also consider bundles
3939
// with the same semantic version but higher release as valid successors.
4040
// This allows upgrades to re-released bundles (e.g., 2.0.0+1 -> 2.0.0+2).
41-
if features.OperatorControllerFeatureGate.Enabled(features.ReleaseVersionPriority) {
41+
if features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison) {
4242
predicates = append(predicates, SameVersionHigherRelease(*installedVersionRelease))
4343
}
4444

internal/operator-controller/catalogmetadata/filter/successors_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,14 @@ func TestLegacySuccessor(t *testing.T) {
243243
assert.False(t, f(emptyBundle))
244244
}
245245

246-
// TestSuccessorsOf_WithReleaseVersionPriority_FeatureGateDisabled verifies higher releases
247-
// are NOT successors when ReleaseVersionPriority gate is disabled (testing the default behavior).
248-
func TestSuccessorsOf_WithReleaseVersionPriority_FeatureGateDisabled(t *testing.T) {
246+
// TestSuccessorsOf_WithCompositeVersionComparison_FeatureGateDisabled verifies higher releases
247+
// are NOT successors when CompositeVersionComparison gate is disabled (testing the default behavior).
248+
func TestSuccessorsOf_WithCompositeVersionComparison_FeatureGateDisabled(t *testing.T) {
249249
// Explicitly disable the feature gate for this test
250-
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.ReleaseVersionPriority)
251-
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.ReleaseVersionPriority)))
250+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison)
251+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.CompositeVersionComparison)))
252252
t.Cleanup(func() {
253-
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.ReleaseVersionPriority, prevEnabled)))
253+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.CompositeVersionComparison, prevEnabled)))
254254
})
255255

256256
channel := declcfg.Channel{
@@ -282,14 +282,14 @@ func TestSuccessorsOf_WithReleaseVersionPriority_FeatureGateDisabled(t *testing.
282282
assert.False(t, predicate(higherRelease))
283283
}
284284

285-
// TestSuccessorsOf_WithReleaseVersionPriority_FeatureGateEnabled verifies higher releases
286-
// as valid successors when ReleaseVersionPriority gate is enabled.
287-
func TestSuccessorsOf_WithReleaseVersionPriority_FeatureGateEnabled(t *testing.T) {
285+
// TestSuccessorsOf_WithCompositeVersionComparison_FeatureGateEnabled verifies higher releases
286+
// as valid successors when CompositeVersionComparison gate is enabled.
287+
func TestSuccessorsOf_WithCompositeVersionComparison_FeatureGateEnabled(t *testing.T) {
288288
// Enable the feature gate for this test
289-
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.ReleaseVersionPriority)
290-
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.ReleaseVersionPriority)))
289+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison)
290+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.CompositeVersionComparison)))
291291
t.Cleanup(func() {
292-
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.ReleaseVersionPriority, prevEnabled)))
292+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.CompositeVersionComparison, prevEnabled)))
293293
})
294294

295295
channel := declcfg.Channel{

internal/operator-controller/features/features.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ const (
1717
WebhookProviderCertManager featuregate.Feature = "WebhookProviderCertManager"
1818
WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA"
1919
HelmChartSupport featuregate.Feature = "HelmChartSupport"
20-
BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime"
21-
DeploymentConfig featuregate.Feature = "DeploymentConfig"
22-
ReleaseVersionPriority featuregate.Feature = "ReleaseVersionPriority"
20+
BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime"
21+
DeploymentConfig featuregate.Feature = "DeploymentConfig"
22+
CompositeVersionComparison featuregate.Feature = "CompositeVersionComparison"
2323
)
2424

2525
var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -91,11 +91,12 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
9191
LockToDefault: false,
9292
},
9393

94-
// ReleaseVersionPriority enables considering bundles with higher release versions
95-
// as valid upgrade successors. When enabled, bundles with the same semantic version
96-
// but higher release values (e.g., 2.0.0+2 as a successor to 2.0.0+1) are included
97-
// in the upgrade candidate set during resolution.
98-
ReleaseVersionPriority: {
94+
// CompositeVersionComparison enables bundle comparison using CompositeVersion
95+
// from operator-registry, which reads the explicit pkg.Release field from the
96+
// olm.package property. When this comparison returns equal, build metadata is
97+
// used as a tiebreaker. This supports both new bundle formats (with explicit
98+
// release) and registry+v1 bundles (with build metadata).
99+
CompositeVersionComparison: {
99100
Default: false,
100101
PreRelease: featuregate.Alpha,
101102
LockToDefault: false,

0 commit comments

Comments
 (0)