Skip to content

Commit 3af1ee4

Browse files
address latest review feedback
Signed-off-by: Rashmi Gottipati <rgottipa@redhat.com>
1 parent 7b18170 commit 3af1ee4

5 files changed

Lines changed: 186 additions & 90 deletions

File tree

internal/operator-controller/bundleutil/bundle.go

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,19 @@ func parseVersionRelease(pkgData json.RawMessage) (*bundle.VersionRelease, error
2929
return nil, fmt.Errorf("error unmarshalling package property: %w", err)
3030
}
3131

32+
// Check if release field is explicitly present in JSON (even if empty).
33+
// property.Package has Release string, so we can't distinguish "field absent" from "field empty".
34+
// We unmarshal again into a helper struct with Release *string to detect presence.
35+
var releaseField struct {
36+
Release *string `json:"release"`
37+
}
38+
if err := json.Unmarshal(pkgData, &releaseField); err != nil {
39+
return nil, fmt.Errorf("error unmarshalling package release field: %w", err)
40+
}
41+
3242
// When BundleReleaseSupport is enabled and bundle has explicit release field, use it.
33-
// Note: Build metadata is preserved here because with an explicit release field,
34-
// build metadata serves its proper semver purpose (e.g., git commit, build number).
35-
// In contrast, NewLegacyRegistryV1VersionRelease clears build metadata because it
36-
// interprets build metadata AS the release value for registry+v1 bundles.
37-
if features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport) && pkg.Release != "" {
38-
vers, err := bsemver.Parse(pkg.Version)
39-
if err != nil {
40-
return nil, fmt.Errorf("error parsing version %q: %w", pkg.Version, err)
41-
}
42-
rel, err := bundle.NewRelease(pkg.Release)
43-
if err != nil {
44-
return nil, fmt.Errorf("error parsing release %q: %w", pkg.Release, err)
45-
}
46-
return &bundle.VersionRelease{
47-
Version: vers,
48-
Release: rel,
49-
}, nil
43+
if features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport) && releaseField.Release != nil {
44+
return parseExplicitRelease(pkg.Version, *releaseField.Release)
5045
}
5146

5247
// Fall back to legacy registry+v1 behavior (release in build metadata)
@@ -62,15 +57,53 @@ func parseVersionRelease(pkgData json.RawMessage) (*bundle.VersionRelease, error
6257
return vr, nil
6358
}
6459

60+
// parseExplicitRelease parses version and release from separate fields.
61+
// Build metadata is preserved in the version because with an explicit release field,
62+
// build metadata serves its proper semver purpose (e.g., git commit, build number).
63+
// In contrast, NewLegacyRegistryV1VersionRelease clears build metadata because it
64+
// interprets build metadata AS the release value for registry+v1 bundles.
65+
func parseExplicitRelease(version, releaseStr string) (*bundle.VersionRelease, error) {
66+
vers, err := bsemver.Parse(version)
67+
if err != nil {
68+
return nil, fmt.Errorf("error parsing version %q: %w", version, err)
69+
}
70+
71+
var rel bundle.Release
72+
if releaseStr == "" {
73+
// Explicit empty release: use empty slice (not nil)
74+
rel = bundle.Release([]bsemver.PRVersion{})
75+
} else {
76+
rel, err = bundle.NewRelease(releaseStr)
77+
if err != nil {
78+
return nil, fmt.Errorf("error parsing release %q: %w", releaseStr, err)
79+
}
80+
}
81+
82+
return &bundle.VersionRelease{
83+
Version: vers,
84+
Release: rel,
85+
}, nil
86+
}
87+
6588
// MetadataFor returns a BundleMetadata for the given bundle name and version/release.
6689
func MetadataFor(bundleName string, vr bundle.VersionRelease) ocv1.BundleMetadata {
67-
bm := ocv1.BundleMetadata{
68-
Name: bundleName,
69-
Version: vr.Version.String(),
90+
if features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport) {
91+
// New behavior: separate Version and Release fields
92+
bm := ocv1.BundleMetadata{
93+
Name: bundleName,
94+
Version: vr.Version.String(),
95+
}
96+
if vr.Release != nil {
97+
relStr := vr.Release.String()
98+
bm.Release = &relStr
99+
}
100+
return bm
70101
}
71-
if vr.Release != nil {
72-
relStr := vr.Release.String()
73-
bm.Release = &relStr
102+
// Old behavior for backward compatibility: reconstitute build metadata in Version field
103+
// This preserves release information (e.g., "1.0.0+2") for standard CRD users where
104+
// the Release field is pruned by the API server.
105+
return ocv1.BundleMetadata{
106+
Name: bundleName,
107+
Version: vr.AsLegacyRegistryV1Version().String(),
74108
}
75-
return bm
76109
}

internal/operator-controller/bundleutil/bundle_test.go

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ func TestGetVersionAndRelease_WithBundleReleaseSupport(t *testing.T) {
151151
},
152152
wantErr: true,
153153
},
154+
{
155+
name: "explicit empty release - preserves build metadata in version",
156+
pkgProperty: &property.Property{
157+
Type: property.TypePackage,
158+
Value: json.RawMessage(`{"version": "1.0.0+2", "release": ""}`),
159+
},
160+
wantVersionRelease: &bundle.VersionRelease{
161+
Version: bsemver.MustParse("1.0.0+2"), // Build metadata preserved (not extracted as release)
162+
Release: bundle.Release([]bsemver.PRVersion{}), // Explicit empty release
163+
},
164+
wantErr: false,
165+
},
154166
}
155167

156168
for _, tc := range tests {
@@ -205,38 +217,76 @@ func TestGetVersionAndRelease_WithBundleReleaseSupport(t *testing.T) {
205217
}
206218

207219
func TestMetadataFor(t *testing.T) {
208-
t.Run("with release", func(t *testing.T) {
209-
vr := bundle.VersionRelease{
210-
Version: bsemver.MustParse("1.0.0"),
211-
Release: bundle.Release([]bsemver.PRVersion{{VersionNum: 2, IsNum: true}}),
212-
}
213-
result := bundleutil.MetadataFor("test-bundle", vr)
214-
require.Equal(t, "test-bundle", result.Name)
215-
require.Equal(t, "1.0.0", result.Version)
216-
require.NotNil(t, result.Release)
217-
require.Equal(t, "2", *result.Release)
218-
})
220+
t.Run("with feature gate enabled", func(t *testing.T) {
221+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport)
222+
require.NoError(t, features.OperatorControllerFeatureGate.Set("BundleReleaseSupport=true"))
223+
t.Cleanup(func() {
224+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("BundleReleaseSupport=%t", prevEnabled)))
225+
})
219226

220-
t.Run("without release", func(t *testing.T) {
221-
vr := bundle.VersionRelease{
222-
Version: bsemver.MustParse("1.0.0"),
223-
Release: nil,
224-
}
225-
result := bundleutil.MetadataFor("test-bundle", vr)
226-
require.Equal(t, "test-bundle", result.Name)
227-
require.Equal(t, "1.0.0", result.Version)
228-
require.Nil(t, result.Release)
227+
t.Run("with release", func(t *testing.T) {
228+
vr := bundle.VersionRelease{
229+
Version: bsemver.MustParse("1.0.0"),
230+
Release: bundle.Release([]bsemver.PRVersion{{VersionNum: 2, IsNum: true}}),
231+
}
232+
result := bundleutil.MetadataFor("test-bundle", vr)
233+
require.Equal(t, "test-bundle", result.Name)
234+
require.Equal(t, "1.0.0", result.Version)
235+
require.NotNil(t, result.Release)
236+
require.Equal(t, "2", *result.Release)
237+
})
238+
239+
t.Run("without release", func(t *testing.T) {
240+
vr := bundle.VersionRelease{
241+
Version: bsemver.MustParse("1.0.0"),
242+
Release: nil,
243+
}
244+
result := bundleutil.MetadataFor("test-bundle", vr)
245+
require.Equal(t, "test-bundle", result.Name)
246+
require.Equal(t, "1.0.0", result.Version)
247+
require.Nil(t, result.Release)
248+
})
249+
250+
t.Run("with explicit empty release", func(t *testing.T) {
251+
vr := bundle.VersionRelease{
252+
Version: bsemver.MustParse("1.0.0"),
253+
Release: bundle.Release([]bsemver.PRVersion{}),
254+
}
255+
result := bundleutil.MetadataFor("test-bundle", vr)
256+
require.Equal(t, "test-bundle", result.Name)
257+
require.Equal(t, "1.0.0", result.Version)
258+
require.NotNil(t, result.Release)
259+
require.Empty(t, *result.Release)
260+
})
229261
})
230262

231-
t.Run("with explicit empty release", func(t *testing.T) {
232-
vr := bundle.VersionRelease{
233-
Version: bsemver.MustParse("1.0.0"),
234-
Release: bundle.Release([]bsemver.PRVersion{}),
235-
}
236-
result := bundleutil.MetadataFor("test-bundle", vr)
237-
require.Equal(t, "test-bundle", result.Name)
238-
require.Equal(t, "1.0.0", result.Version)
239-
require.NotNil(t, result.Release)
240-
require.Empty(t, *result.Release)
263+
t.Run("with feature gate disabled (legacy mode)", func(t *testing.T) {
264+
prevEnabled := features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport)
265+
require.NoError(t, features.OperatorControllerFeatureGate.Set("BundleReleaseSupport=false"))
266+
t.Cleanup(func() {
267+
require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("BundleReleaseSupport=%t", prevEnabled)))
268+
})
269+
270+
t.Run("reconstitutes build metadata in version", func(t *testing.T) {
271+
vr := bundle.VersionRelease{
272+
Version: bsemver.MustParse("1.0.0"),
273+
Release: bundle.Release([]bsemver.PRVersion{{VersionNum: 2, IsNum: true}}),
274+
}
275+
result := bundleutil.MetadataFor("test-bundle", vr)
276+
require.Equal(t, "test-bundle", result.Name)
277+
require.Equal(t, "1.0.0+2", result.Version) // Build metadata reconstituted
278+
require.Nil(t, result.Release) // Release field not used in legacy mode
279+
})
280+
281+
t.Run("preserves original build metadata when no release", func(t *testing.T) {
282+
vr := bundle.VersionRelease{
283+
Version: bsemver.MustParse("1.0.0"),
284+
Release: nil,
285+
}
286+
result := bundleutil.MetadataFor("test-bundle", vr)
287+
require.Equal(t, "test-bundle", result.Name)
288+
require.Equal(t, "1.0.0", result.Version)
289+
require.Nil(t, result.Release)
290+
})
241291
})
242292
}

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

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,54 @@ import (
1212
"github.com/operator-framework/operator-controller/internal/shared/util/filter"
1313
)
1414

15-
func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (filter.Predicate[declcfg.Bundle], error) {
16-
// Construct VersionRelease from BundleMetadata.
17-
// If the Release field is not nil (even if empty string), use it as the explicit release.
18-
// If the Release field is nil, parse release from version build metadata (registry+v1 legacy format).
19-
var installedVersionRelease *bundle.VersionRelease
20-
var err error
21-
22-
if installedBundle.Release != nil {
23-
// Bundle has explicit release field (or explicitly empty) - parse version and release from separate fields.
24-
// Note: We can't use NewLegacyRegistryV1VersionRelease here because the version might
25-
// already contain build metadata (e.g., "1.0.0+git.abc"), which serves its proper
26-
// semver purpose when using explicit pkg.Release. Concatenating would create invalid
27-
// semver like "1.0.0+git.abc+2".
28-
version, err := bsemver.Parse(installedBundle.Version)
15+
// parseInstalledBundleVersionRelease constructs a VersionRelease from BundleMetadata.
16+
// If the Release field is not nil (even if empty string), use it as the explicit release.
17+
// If the Release field is nil, parse release from version build metadata (registry+v1 legacy format).
18+
func parseInstalledBundleVersionRelease(installedBundle ocv1.BundleMetadata) (*bundle.VersionRelease, error) {
19+
// Handle legacy registry+v1 format: release embedded in version's build metadata
20+
if installedBundle.Release == nil {
21+
vr, err := bundle.NewLegacyRegistryV1VersionRelease(installedBundle.Version)
2922
if err != nil {
30-
return nil, fmt.Errorf("failed to parse installed bundle version %q: %w", installedBundle.Version, err)
31-
}
32-
release, err := bundle.NewRelease(*installedBundle.Release)
33-
if err != nil {
34-
return nil, fmt.Errorf("failed to parse installed bundle release %q: %w", *installedBundle.Release, err)
35-
}
36-
installedVersionRelease = &bundle.VersionRelease{
37-
Version: version,
38-
Release: release,
23+
return nil, fmt.Errorf("failed to get version and release of installed bundle: %w", err)
3924
}
40-
} else {
41-
// Legacy registry+v1: release embedded in version's build metadata
42-
installedVersionRelease, err = bundle.NewLegacyRegistryV1VersionRelease(installedBundle.Version)
25+
return vr, nil
26+
}
27+
28+
// Bundle has explicit release field (or explicitly empty) - parse version and release from separate fields.
29+
// Note: We can't use NewLegacyRegistryV1VersionRelease here because the version might
30+
// already contain build metadata (e.g., "1.0.0+git.abc"), which serves its proper
31+
// semver purpose when using explicit pkg.Release. Concatenating would create invalid
32+
// semver like "1.0.0+git.abc+2".
33+
version, err := bsemver.Parse(installedBundle.Version)
34+
if err != nil {
35+
return nil, fmt.Errorf("failed to parse installed bundle version %q: %w", installedBundle.Version, err)
36+
}
37+
38+
// if version has build metadata but release is empty, this is likely
39+
// old-format data (e.g., "1.0.0+2" with Release="") that should be parsed as legacy.
40+
// This handles edge case scenarios.
41+
if len(version.Build) > 0 && *installedBundle.Release == "" {
42+
vr, err := bundle.NewLegacyRegistryV1VersionRelease(installedBundle.Version)
4343
if err != nil {
4444
return nil, fmt.Errorf("failed to get version and release of installed bundle: %w", err)
4545
}
46+
return vr, nil
47+
}
48+
49+
release, err := bundle.NewRelease(*installedBundle.Release)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to parse installed bundle release %q: %w", *installedBundle.Release, err)
52+
}
53+
return &bundle.VersionRelease{
54+
Version: version,
55+
Release: release,
56+
}, nil
57+
}
58+
59+
func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (filter.Predicate[declcfg.Bundle], error) {
60+
installedVersionRelease, err := parseInstalledBundleVersionRelease(installedBundle)
61+
if err != nil {
62+
return nil, err
4663
}
4764

4865
successorsPredicate, err := legacySuccessor(installedBundle, channels...)

internal/operator-controller/controllers/boxcutter_reconcile_steps.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,14 @@ func MigrateStorage(m StorageMigrator) ReconcileStepFunc {
103103
func ApplyBundleWithBoxcutter(apply func(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error)) ReconcileStepFunc {
104104
return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
105105
l := log.FromContext(ctx)
106-
releaseValue := ""
107-
if state.resolvedRevisionMetadata.Release != nil {
108-
releaseValue = *state.resolvedRevisionMetadata.Release
109-
}
110106
revisionAnnotations := map[string]string{
111107
labels.BundleNameKey: state.resolvedRevisionMetadata.Name,
112108
labels.PackageNameKey: state.resolvedRevisionMetadata.Package,
113109
labels.BundleVersionKey: state.resolvedRevisionMetadata.Version,
114110
labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image,
115-
labels.BundleReleaseKey: releaseValue,
111+
}
112+
if state.resolvedRevisionMetadata.Release != nil {
113+
revisionAnnotations[labels.BundleReleaseKey] = *state.resolvedRevisionMetadata.Release
116114
}
117115
objLbls := map[string]string{
118116
labels.OwnerKindKey: ocv1.ClusterExtensionKind,

internal/operator-controller/controllers/clusterextension_reconcile_steps.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -408,16 +408,14 @@ func UnpackBundle(i imageutil.Puller, cache imageutil.Cache) ReconcileStepFunc {
408408
func ApplyBundle(a Applier) ReconcileStepFunc {
409409
return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
410410
l := log.FromContext(ctx)
411-
releaseValue := ""
412-
if state.resolvedRevisionMetadata.Release != nil {
413-
releaseValue = *state.resolvedRevisionMetadata.Release
414-
}
415411
revisionAnnotations := map[string]string{
416412
labels.BundleNameKey: state.resolvedRevisionMetadata.Name,
417413
labels.PackageNameKey: state.resolvedRevisionMetadata.Package,
418414
labels.BundleVersionKey: state.resolvedRevisionMetadata.Version,
419415
labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image,
420-
labels.BundleReleaseKey: releaseValue,
416+
}
417+
if state.resolvedRevisionMetadata.Release != nil {
418+
revisionAnnotations[labels.BundleReleaseKey] = *state.resolvedRevisionMetadata.Release
421419
}
422420
objLbls := map[string]string{
423421
labels.OwnerKindKey: ocv1.ClusterExtensionKind,

0 commit comments

Comments
 (0)