Skip to content

Commit 199f9cc

Browse files
Support explicit pkg.Release field with build metadata fallback
Signed-off-by: Rashmi Gottipati <rgottipa@redhat.com>
1 parent fd25bf7 commit 199f9cc

File tree

9 files changed

+396
-30
lines changed

9 files changed

+396
-30
lines changed

internal/operator-controller/bundleutil/bundle.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,42 @@ import (
1616
func GetVersionAndRelease(b declcfg.Bundle) (*bundle.VersionRelease, error) {
1717
for _, p := range b.Properties {
1818
if p.Type == property.TypePackage {
19-
var pkg property.Package
20-
if err := json.Unmarshal(p.Value, &pkg); err != nil {
21-
return nil, fmt.Errorf("error unmarshalling package property: %w", err)
22-
}
23-
24-
// TODO: For now, we assume that all bundles are registry+v1 bundles.
25-
// In the future, when we support other bundle formats, we should stop
26-
// using the legacy mechanism (i.e. using build metadata in the version)
27-
// to determine the bundle's release.
28-
vr, err := bundle.NewLegacyRegistryV1VersionRelease(pkg.Version)
29-
if err != nil {
30-
return nil, err
31-
}
32-
return vr, nil
19+
return parseVersionRelease(p.Value)
3320
}
3421
}
3522
return nil, fmt.Errorf("no package property found in bundle %q", b.Name)
3623
}
3724

25+
func parseVersionRelease(pkgData json.RawMessage) (*bundle.VersionRelease, error) {
26+
var pkg property.Package
27+
if err := json.Unmarshal(pkgData, &pkg); err != nil {
28+
return nil, fmt.Errorf("error unmarshalling package property: %w", err)
29+
}
30+
31+
// If the bundle has an explicit release field, use it
32+
if pkg.Release != "" {
33+
vers, err := bsemver.Parse(pkg.Version)
34+
if err != nil {
35+
return nil, fmt.Errorf("error parsing version %q: %w", pkg.Version, err)
36+
}
37+
rel, err := bundle.NewRelease(pkg.Release)
38+
if err != nil {
39+
return nil, fmt.Errorf("error parsing release %q: %w", pkg.Release, err)
40+
}
41+
return &bundle.VersionRelease{
42+
Version: vers,
43+
Release: rel,
44+
}, nil
45+
}
46+
47+
// Fall back to legacy registry+v1 behavior (release in build metadata)
48+
vr, err := bundle.NewLegacyRegistryV1VersionRelease(pkg.Version)
49+
if err != nil {
50+
return nil, err
51+
}
52+
return vr, nil
53+
}
54+
3855
// MetadataFor returns a BundleMetadata for the given bundle name and version.
3956
func MetadataFor(bundleName string, bundleVersion bsemver.Version) ocv1.BundleMetadata {
4057
return ocv1.BundleMetadata{

internal/operator-controller/bundleutil/bundle_test.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,44 @@ func TestGetVersionAndRelease(t *testing.T) {
6969
pkgProperty: nil,
7070
wantErr: true,
7171
},
72+
{
73+
name: "explicit release field - takes precedence over build metadata",
74+
pkgProperty: &property.Property{
75+
Type: property.TypePackage,
76+
Value: json.RawMessage(`{"version": "1.0.0+ignored", "release": "2"}`),
77+
},
78+
wantVersionRelease: &bundle.VersionRelease{
79+
Version: bsemver.MustParse("1.0.0+ignored"),
80+
Release: bundle.Release([]bsemver.PRVersion{
81+
{VersionNum: 2, IsNum: true},
82+
}),
83+
},
84+
wantErr: false,
85+
},
86+
{
87+
name: "explicit release field - complex release",
88+
pkgProperty: &property.Property{
89+
Type: property.TypePackage,
90+
Value: json.RawMessage(`{"version": "2.1.0", "release": "1.alpha.3"}`),
91+
},
92+
wantVersionRelease: &bundle.VersionRelease{
93+
Version: bsemver.MustParse("2.1.0"),
94+
Release: bundle.Release([]bsemver.PRVersion{
95+
{VersionNum: 1, IsNum: true},
96+
{VersionStr: "alpha"},
97+
{VersionNum: 3, IsNum: true},
98+
}),
99+
},
100+
wantErr: false,
101+
},
102+
{
103+
name: "explicit release field - invalid release",
104+
pkgProperty: &property.Property{
105+
Type: property.TypePackage,
106+
Value: json.RawMessage(`{"version": "1.0.0", "release": "001"}`),
107+
},
108+
wantErr: true,
109+
},
72110
}
73111

74112
for _, tc := range tests {
@@ -83,11 +121,12 @@ func TestGetVersionAndRelease(t *testing.T) {
83121
Properties: properties,
84122
}
85123

86-
_, err := bundleutil.GetVersionAndRelease(bundle)
124+
actual, err := bundleutil.GetVersionAndRelease(bundle)
87125
if tc.wantErr {
88126
require.Error(t, err)
89127
} else {
90128
require.NoError(t, err)
129+
require.Equal(t, tc.wantVersionRelease, actual)
91130
}
92131
})
93132
}

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: 67 additions & 8 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) {
@@ -138,13 +140,13 @@ func TestByVersionAndRelease(t *testing.T) {
138140
t.Run("all bundles valid", func(t *testing.T) {
139141
toSort := []declcfg.Bundle{b3_1, b2, b3_2, b1}
140142
slices.SortStableFunc(toSort, compare.ByVersionAndRelease)
141-
assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2}, toSort)
143+
assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2}, toSort, "should sort descending: 1.0.0 > 1.0.0-alpha+2 > 1.0.0-alpha+1 > 0.0.1")
142144
})
143145

144146
t.Run("some bundles are missing version", func(t *testing.T) {
145147
toSort := []declcfg.Bundle{b3_1, b4noVersion, b2, b3_2, b5empty, b1}
146148
slices.SortStableFunc(toSort, compare.ByVersionAndRelease)
147-
assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2, b4noVersion, b5empty}, toSort)
149+
assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2, b4noVersion, b5empty}, toSort, "bundles with invalid/missing versions should sort last")
148150
})
149151
}
150152

@@ -161,10 +163,67 @@ func TestByDeprecationFunc(t *testing.T) {
161163
c := declcfg.Bundle{Name: "c"}
162164
d := declcfg.Bundle{Name: "d"}
163165

164-
assert.Equal(t, 0, byDeprecation(a, b))
165-
assert.Equal(t, 0, byDeprecation(b, a))
166-
assert.Equal(t, 1, byDeprecation(a, c))
167-
assert.Equal(t, -1, byDeprecation(c, a))
168-
assert.Equal(t, 0, byDeprecation(c, d))
169-
assert.Equal(t, 0, byDeprecation(d, c))
166+
assert.Equal(t, 0, byDeprecation(a, b), "both deprecated bundles are equal")
167+
assert.Equal(t, 0, byDeprecation(b, a), "both deprecated bundles are equal")
168+
assert.Equal(t, 1, byDeprecation(a, c), "deprecated bundle 'a' should sort after non-deprecated 'c'")
169+
assert.Equal(t, -1, byDeprecation(c, a), "non-deprecated bundle 'c' should sort before deprecated 'a'")
170+
assert.Equal(t, 0, byDeprecation(c, d), "both non-deprecated bundles are equal")
171+
assert.Equal(t, 0, byDeprecation(d, c), "both non-deprecated bundles are equal")
172+
}
173+
174+
// TestByVersionAndRelease_WithCompositeVersionComparison tests the feature-gated hybrid comparison
175+
// for registry+v1 bundles. When the feature gate is enabled, Bundle.Compare() is called but returns
176+
// 0 for registry+v1 bundles (no explicit release field), so we fall back to build metadata parsing.
177+
// This maintains backward compatibility while preparing for future bundle formats with explicit release.
178+
func TestByVersionAndRelease_WithCompositeVersionComparison(t *testing.T) {
179+
// Registry+v1 bundles: same version, different build metadata
180+
b1 := declcfg.Bundle{
181+
Name: "package1.v1.0.0+1",
182+
Properties: []property.Property{
183+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0+1"}`)},
184+
},
185+
}
186+
b2 := declcfg.Bundle{
187+
Name: "package1.v1.0.0+2",
188+
Properties: []property.Property{
189+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0+2"}`)},
190+
},
191+
}
192+
193+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.CompositeVersionComparison)
194+
t.Cleanup(func() {
195+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=%t", features.CompositeVersionComparison, prevEnabled)))
196+
})
197+
198+
t.Run("feature gate disabled - uses build metadata only", func(t *testing.T) {
199+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.CompositeVersionComparison)))
200+
result := compare.ByVersionAndRelease(b1, b2)
201+
assert.Positive(t, result, "should sort by build metadata: 1.0.0+2 > 1.0.0+1")
202+
})
203+
204+
t.Run("feature gate enabled - registry+v1 bundles still work via build metadata fallback", func(t *testing.T) {
205+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.CompositeVersionComparison)))
206+
result := compare.ByVersionAndRelease(b1, b2)
207+
assert.Positive(t, result, "Bundle.Compare() returns 0, fallback to build metadata: 1.0.0+2 > 1.0.0+1")
208+
})
209+
210+
t.Run("bundles with explicit pkg.Release field", func(t *testing.T) {
211+
// Bundle with explicit release=1
212+
explicitR1 := declcfg.Bundle{
213+
Name: "pkg.v2.0.0-r1",
214+
Properties: []property.Property{
215+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "pkg", "version": "2.0.0", "release": "1"}`)},
216+
},
217+
}
218+
// Bundle with explicit release=2
219+
explicitR2 := declcfg.Bundle{
220+
Name: "pkg.v2.0.0-r2",
221+
Properties: []property.Property{
222+
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "pkg", "version": "2.0.0", "release": "2"}`)},
223+
},
224+
}
225+
226+
result := compare.ByVersionAndRelease(explicitR1, explicitR2)
227+
assert.Positive(t, result, "2.0.0+release.2 > 2.0.0+release.1 (explicit release field)")
228+
})
170229
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,21 @@ func InAnyChannel(channels ...declcfg.Channel) filter.Predicate[declcfg.Bundle]
4848
return false
4949
}
5050
}
51+
52+
// SameVersionHigherRelease returns a predicate that matches bundles with the same
53+
// semantic version as the provided version-release, but with a higher release value.
54+
// This is used to identify re-released bundles (e.g., 2.0.0+2 when 2.0.0+1 is installed).
55+
func SameVersionHigherRelease(expect bundle.VersionRelease) filter.Predicate[declcfg.Bundle] {
56+
return func(b declcfg.Bundle) bool {
57+
actual, err := bundleutil.GetVersionAndRelease(b)
58+
if err != nil {
59+
return false
60+
}
61+
62+
if expect.Version.Compare(actual.Version) != 0 {
63+
return false
64+
}
65+
66+
return expect.Release.Compare(actual.Release) < 0
67+
}
68+
}

0 commit comments

Comments
 (0)