Skip to content

Commit 8f24e45

Browse files
adds support for parsing explicit pkg.Release field
Signed-off-by: Rashmi Gottipati <rgottipa@redhat.com>
1 parent dd2e1f6 commit 8f24e45

20 files changed

Lines changed: 658 additions & 39 deletions

File tree

‎api/v1/clusterextension_types.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,29 @@ type BundleMetadata struct {
466466
// +required
467467
// +kubebuilder:validation:XValidation:rule="self.matches(\"^([0-9]+)(\\\\.[0-9]+)?(\\\\.[0-9]+)?(-([-0-9A-Za-z]+(\\\\.[-0-9A-Za-z]+)*))?(\\\\+([-0-9A-Za-z]+(-\\\\.[-0-9A-Za-z]+)*))?\")",message="version must be well-formed semver"
468468
Version string `json:"version"`
469+
470+
// release is an optional field that identifies a specific release of this bundle's version.
471+
// A release represents a re-publication of the same version, typically used to deliver
472+
// packaging or metadata changes without changing the version number. When multiple
473+
// releases exist for the same version, higher releases are preferred. An unset release
474+
// is less preferred than all other release values.
475+
//
476+
// The value consists of dot-separated identifiers, where each identifier is either a
477+
// numeric value (without leading zeros) or an alphanumeric string (e.g., "2", "1.el9",
478+
// "3.alpha.1"). Releases are compared identifier by identifier: numeric identifiers are
479+
// compared as integers, alphanumeric identifiers are compared lexically, and numeric
480+
// identifiers always sort before alphanumeric identifiers.
481+
//
482+
// For bundles with explicit pkg.Release metadata, this field contains that release value.
483+
// For registry+v1 bundles lacking an explicit release value, this field contains the release
484+
// extracted from version's build metadata (e.g., '2' from '1.0.0+2').
485+
// This field is omitted when the bundle's release value is unset.
486+
//
487+
// +optional
488+
// <opcon:experimental>
489+
// +kubebuilder:validation:MaxLength=20
490+
// +kubebuilder:validation:XValidation:rule="self.matches(\"^$|^(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\\\\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*$\")",message="release must be empty or consist of dot-separated identifiers (numeric without leading zeros, or alphanumeric)"
491+
Release *string `json:"release,omitempty"`
469492
}
470493

471494
// RevisionStatus defines the observed state of a ClusterObjectSet.

‎api/v1/zz_generated.deepcopy.go‎

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎applyconfigurations/api/v1/bundlemetadata.go‎

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/api-reference/olmv1-api-reference.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ _Appears in:_
6767
| --- | --- | --- | --- |
6868
| `name` _string_ | name is required and follows the DNS subdomain standard as defined in [RFC 1123].<br />It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.),<br />start and end with an alphanumeric character, and be no longer than 253 characters. | | Required: \{\} <br /> |
6969
| `version` _string_ | version is required and references the version that this bundle represents.<br />It follows the semantic versioning standard as defined in https://semver.org/. | | Required: \{\} <br /> |
70+
| `release` _string_ | release is an optional field that identifies a specific release of this bundle's version.<br />A release represents a re-publication of the same version, typically used to deliver<br />packaging or metadata changes without changing the version number. When multiple<br />releases exist for the same version, higher releases are preferred. An unset release<br />is less preferred than all other release values.<br />The value consists of dot-separated identifiers, where each identifier is either a<br />numeric value (without leading zeros) or an alphanumeric string (e.g., "2", "1.el9",<br />"3.alpha.1"). Releases are compared identifier by identifier: numeric identifiers are<br />compared as integers, alphanumeric identifiers are compared lexically, and numeric<br />identifiers always sort before alphanumeric identifiers.<br />For bundles with explicit pkg.Release metadata, this field contains that release value.<br />For registry+v1 bundles lacking an explicit release value, this field contains the release<br />extracted from version's build metadata (e.g., '2' from '1.0.0+2').<br />This field is omitted when the bundle's release value is unset.<br /><opcon:experimental> | | MaxLength: 20 <br />Optional: \{\} <br /> |
7071

7172

7273
#### CRDUpgradeSafetyEnforcement

‎helm/experimental.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ options:
1414
- HelmChartSupport
1515
- BoxcutterRuntime
1616
- DeploymentConfig
17+
- BundleReleaseSupport
1718
disabled:
1819
- WebhookProviderOpenshiftServiceCA
1920
# List of enabled experimental features for catalogd

‎helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,30 @@ spec:
688688
hyphens (-) or periods (.), start and end with an alphanumeric
689689
character, and be no longer than 253 characters
690690
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$")
691+
release:
692+
description: |-
693+
release is an optional field that identifies a specific release of this bundle's version.
694+
A release represents a re-publication of the same version, typically used to deliver
695+
packaging or metadata changes without changing the version number. When multiple
696+
releases exist for the same version, higher releases are preferred. An unset release
697+
is less preferred than all other release values.
698+
699+
The value consists of dot-separated identifiers, where each identifier is either a
700+
numeric value (without leading zeros) or an alphanumeric string (e.g., "2", "1.el9",
701+
"3.alpha.1"). Releases are compared identifier by identifier: numeric identifiers are
702+
compared as integers, alphanumeric identifiers are compared lexically, and numeric
703+
identifiers always sort before alphanumeric identifiers.
704+
705+
For bundles with explicit pkg.Release metadata, this field contains that release value.
706+
For registry+v1 bundles lacking an explicit release value, this field contains the release
707+
extracted from version's build metadata (e.g., '2' from '1.0.0+2').
708+
This field is omitted when the bundle's release value is unset.
709+
maxLength: 20
710+
type: string
711+
x-kubernetes-validations:
712+
- message: release must be empty or consist of dot-separated
713+
identifiers (numeric without leading zeros, or alphanumeric)
714+
rule: self.matches("^$|^(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*$")
691715
version:
692716
description: |-
693717
version is required and references the version that this bundle represents.

‎internal/operator-controller/applier/boxcutter.go‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
8585
if v := helmRelease.Labels[labels.BundleVersionKey]; v != "" {
8686
annotationUpdates[labels.BundleVersionKey] = v
8787
}
88+
if v := helmRelease.Labels[labels.BundleReleaseKey]; v != "" {
89+
annotationUpdates[labels.BundleReleaseKey] = v
90+
}
8891
if v := helmRelease.Labels[labels.PackageNameKey]; v != "" {
8992
annotationUpdates[labels.PackageNameKey] = v
9093
}
@@ -96,12 +99,16 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
9699
WithObject(obj))
97100
}
98101

99-
rev := r.buildClusterObjectSet(objs, ext, map[string]string{
102+
revisionAnnotations := map[string]string{
100103
labels.BundleNameKey: helmRelease.Labels[labels.BundleNameKey],
101104
labels.PackageNameKey: helmRelease.Labels[labels.PackageNameKey],
102105
labels.BundleVersionKey: helmRelease.Labels[labels.BundleVersionKey],
103106
labels.BundleReferenceKey: helmRelease.Labels[labels.BundleReferenceKey],
104-
})
107+
}
108+
if v := helmRelease.Labels[labels.BundleReleaseKey]; v != "" {
109+
revisionAnnotations[labels.BundleReleaseKey] = v
110+
}
111+
rev := r.buildClusterObjectSet(objs, ext, revisionAnnotations)
105112
rev.WithName(fmt.Sprintf("%s-1", ext.Name))
106113
rev.Spec.WithRevision(1)
107114
rev.Spec.WithCollisionProtection(ocv1.CollisionProtectionNone) // allow to adopt objects from previous release

‎internal/operator-controller/bundle/versionrelease.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ func (vr *VersionRelease) AsLegacyRegistryV1Version() bsemver.Version {
8585

8686
type Release []bsemver.PRVersion
8787

88+
// String returns the string representation of the release.
89+
// Returns an empty string if the release is nil or empty.
90+
func (r Release) String() string {
91+
if len(r) == 0 {
92+
return ""
93+
}
94+
parts := make([]string, len(r))
95+
for i, pr := range r {
96+
parts[i] = pr.String()
97+
}
98+
return strings.Join(parts, ".")
99+
}
100+
88101
// Compare compares two Release values. It returns:
89102
//
90103
// -1 if r < other

‎internal/operator-controller/bundleutil/bundle.go‎

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,99 @@ import (
1111

1212
ocv1 "github.com/operator-framework/operator-controller/api/v1"
1313
"github.com/operator-framework/operator-controller/internal/operator-controller/bundle"
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
1415
)
1516

1617
func GetVersionAndRelease(b declcfg.Bundle) (*bundle.VersionRelease, error) {
1718
for _, p := range b.Properties {
1819
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
20+
return parseVersionRelease(p.Value)
3321
}
3422
}
3523
return nil, fmt.Errorf("no package property found in bundle %q", b.Name)
3624
}
3725

38-
// MetadataFor returns a BundleMetadata for the given bundle name and version.
39-
func MetadataFor(bundleName string, bundleVersion bsemver.Version) ocv1.BundleMetadata {
26+
func parseVersionRelease(pkgData json.RawMessage) (*bundle.VersionRelease, error) {
27+
var pkg property.Package
28+
if err := json.Unmarshal(pkgData, &pkg); err != nil {
29+
return nil, fmt.Errorf("error unmarshalling package property: %w", err)
30+
}
31+
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+
42+
// When BundleReleaseSupport is enabled and bundle has explicit release field, use it.
43+
if features.OperatorControllerFeatureGate.Enabled(features.BundleReleaseSupport) && releaseField.Release != nil {
44+
return parseExplicitRelease(pkg.Version, *releaseField.Release)
45+
}
46+
47+
// Fall back to legacy registry+v1 behavior (release in build metadata)
48+
//
49+
// TODO: For now, we assume that all bundles are registry+v1 bundles.
50+
// In the future, for supporting other bundle formats, we should not
51+
// use the legacy registry+v1 mechanism (i.e. using build metadata in
52+
// the version) to determine the bundle's release.
53+
vr, err := bundle.NewLegacyRegistryV1VersionRelease(pkg.Version)
54+
if err != nil {
55+
return nil, err
56+
}
57+
return vr, nil
58+
}
59+
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+
88+
// MetadataFor returns a BundleMetadata for the given bundle name and version/release.
89+
func MetadataFor(bundleName string, vr bundle.VersionRelease) ocv1.BundleMetadata {
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
101+
}
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.
40105
return ocv1.BundleMetadata{
41106
Name: bundleName,
42-
Version: bundleVersion.String(),
107+
Version: vr.AsLegacyRegistryV1Version().String(),
43108
}
44109
}

0 commit comments

Comments
 (0)