Skip to content

Commit 72c3a34

Browse files
joelanfordclaude
andcommitted
Add support for build metadata precedence in bundle version comparison
This change fixes an issue to ensure that operator-controller properly handles and compares registry+v1 bundle versions that include build metadata as specified in the semver version. The intention is that we only treat build metadata as a release value for registry+v1 bundles, which already have this precedent set. If/when operator-controller gains support for new bundle types, the intention is to avoid continuing the practice (and semver violation) of treating version build metadata as comparable/orderable. Key changes: - Introduce VersionRelease type combining semver version with release metadata - Update bundle comparison logic to consider build metadata when present - Add exact version matching for pinned versions with build metadata - Replace GetVersion with GetVersionAndRelease across the codebase - Ensure successors are determined based on exact version+release matching This is particularly important for registry+v1 bundles that encode release information in the build metadata field of their version strings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6604f2a commit 72c3a34

24 files changed

+813
-115
lines changed

api/v1/clusterextension_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ type CatalogFilter struct {
235235
// "0.6.0", which means "only install version 0.6.0 and never
236236
// upgrade from this version".
237237
//
238+
// For registry+v1 bundles that include release information in the build
239+
// metadata field (e.g., "1.0.0+20230101"), you can pin to an exact version
240+
// including the release by specifying the full version string with build
241+
// metadata (e.g., "1.0.0+20230101"). This ensures an exact match of both
242+
// the semver version and the release. If you specify a version without build
243+
// metadata (e.g., "1.0.0"), it will match all bundles with that version
244+
// regardless of their release information.
245+
//
238246
// # Basic Comparison Operators
239247
//
240248
// The basic comparison operators and their meanings are:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ _Appears in:_
9797
| Field | Description | Default | Validation |
9898
| --- | --- | --- | --- |
9999
| `packageName` _string_ | packageName is a reference to the name of the package to be installed<br />and is used to filter the content from catalogs.<br /><br />packageName is required, immutable, and follows the DNS subdomain standard<br />as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters,<br />hyphens (-) or periods (.), start and end with an alphanumeric character,<br />and be no longer than 253 characters.<br /><br />Some examples of valid values are:<br /> - some-package<br /> - 123-package<br /> - 1-package-2<br /> - somepackage<br /><br />Some examples of invalid values are:<br /> - -some-package<br /> - some-package-<br /> - thisisareallylongpackagenamethatisgreaterthanthemaximumlength<br /> - some.package<br /><br />[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 253 <br />Required: \{\} <br /> |
100-
| `version` _string_ | version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed.<br /><br />Acceptable version ranges are no longer than 64 characters.<br />Version ranges are composed of comma- or space-delimited values and one or<br />more comparison operators, known as comparison strings. Additional<br />comparison strings can be added using the OR operator (\|\|).<br /><br /># Range Comparisons<br /><br />To specify a version range, you can use a comparison string like ">=3.0,<br /><3.6". When specifying a range, automatic updates will occur within that<br />range. The example comparison string means "install any version greater than<br />or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any<br />upgrades are available within the version range after initial installation,<br />those upgrades should be automatically performed.<br /><br /># Pinned Versions<br /><br />To specify an exact version to install you can use a version range that<br />"pins" to a specific version. When pinning to a specific version, no<br />automatic updates will occur. An example of a pinned version range is<br />"0.6.0", which means "only install version 0.6.0 and never<br />upgrade from this version".<br /><br /># Basic Comparison Operators<br /><br />The basic comparison operators and their meanings are:<br /> - "=", equal (not aliased to an operator)<br /> - "!=", not equal<br /> - "<", less than<br /> - ">", greater than<br /> - ">=", greater than OR equal to<br /> - "<=", less than OR equal to<br /><br /># Wildcard Comparisons<br /><br />You can use the "x", "X", and "*" characters as wildcard characters in all<br />comparison operations. Some examples of using the wildcard characters:<br /> - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0"<br /> - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0"<br /> - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3"<br /> - "x", "X", and "*" is equivalent to ">= 0.0.0"<br /><br /># Patch Release Comparisons<br /><br />When you want to specify a minor version up to the next major version you<br />can use the "~" character to perform patch comparisons. Some examples:<br /> - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0"<br /> - "~1" and "~1.x" is equivalent to ">=1, <2"<br /> - "~2.3" is equivalent to ">=2.3, <2.4"<br /> - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0"<br /><br /># Major Release Comparisons<br /><br />You can use the "^" character to make major release comparisons after a<br />stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples:<br /> - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0"<br /> - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0"<br /> - "^2.3" is equivalent to ">=2.3, <3"<br /> - "^2.x" is equivalent to ">=2.0.0, <3"<br /> - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0"<br /> - "^0.2" is equivalent to ">=0.2.0, <0.3.0"<br /> - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4"<br /> - "^0.0" is equivalent to ">=0.0.0, <0.1.0"<br /> - "^0" is equivalent to ">=0.0.0, <1.0.0"<br /><br /># OR Comparisons<br />You can use the "\|\|" character to represent an OR operation in the version<br />range. Some examples:<br /> - ">=1.2.3, <2.0.0 \|\| >3.0.0"<br /> - "^0 \|\| ^3 \|\| ^5"<br /><br />For more information on semver, please see https://semver.org/ | | MaxLength: 64 <br /> |
100+
| `version` _string_ | version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed.<br /><br />Acceptable version ranges are no longer than 64 characters.<br />Version ranges are composed of comma- or space-delimited values and one or<br />more comparison operators, known as comparison strings. Additional<br />comparison strings can be added using the OR operator (\|\|).<br /><br /># Range Comparisons<br /><br />To specify a version range, you can use a comparison string like ">=3.0,<br /><3.6". When specifying a range, automatic updates will occur within that<br />range. The example comparison string means "install any version greater than<br />or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any<br />upgrades are available within the version range after initial installation,<br />those upgrades should be automatically performed.<br /><br /># Pinned Versions<br /><br />To specify an exact version to install you can use a version range that<br />"pins" to a specific version. When pinning to a specific version, no<br />automatic updates will occur. An example of a pinned version range is<br />"0.6.0", which means "only install version 0.6.0 and never<br />upgrade from this version".<br /><br />For registry+v1 bundles that include release information in the build<br />metadata field (e.g., "1.0.0+20230101"), you can pin to an exact version<br />including the release by specifying the full version string with build<br />metadata (e.g., "1.0.0+20230101"). This ensures an exact match of both<br />the semver version and the release. If you specify a version without build<br />metadata (e.g., "1.0.0"), it will match all bundles with that version<br />regardless of their release information.<br /><br /># Basic Comparison Operators<br /><br />The basic comparison operators and their meanings are:<br /> - "=", equal (not aliased to an operator)<br /> - "!=", not equal<br /> - "<", less than<br /> - ">", greater than<br /> - ">=", greater than OR equal to<br /> - "<=", less than OR equal to<br /><br /># Wildcard Comparisons<br /><br />You can use the "x", "X", and "*" characters as wildcard characters in all<br />comparison operations. Some examples of using the wildcard characters:<br /> - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0"<br /> - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0"<br /> - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3"<br /> - "x", "X", and "*" is equivalent to ">= 0.0.0"<br /><br /># Patch Release Comparisons<br /><br />When you want to specify a minor version up to the next major version you<br />can use the "~" character to perform patch comparisons. Some examples:<br /> - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0"<br /> - "~1" and "~1.x" is equivalent to ">=1, <2"<br /> - "~2.3" is equivalent to ">=2.3, <2.4"<br /> - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0"<br /><br /># Major Release Comparisons<br /><br />You can use the "^" character to make major release comparisons after a<br />stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples:<br /> - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0"<br /> - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0"<br /> - "^2.3" is equivalent to ">=2.3, <3"<br /> - "^2.x" is equivalent to ">=2.0.0, <3"<br /> - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0"<br /> - "^0.2" is equivalent to ">=0.2.0, <0.3.0"<br /> - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4"<br /> - "^0.0" is equivalent to ">=0.0.0, <0.1.0"<br /> - "^0" is equivalent to ">=0.0.0, <1.0.0"<br /><br /># OR Comparisons<br />You can use the "\|\|" character to represent an OR operation in the version<br />range. Some examples:<br /> - ">=1.2.3, <2.0.0 \|\| >3.0.0"<br /> - "^0 \|\| ^3 \|\| ^5"<br /><br />For more information on semver, please see https://semver.org/ | | MaxLength: 64 <br /> |
101101
| `channels` _string array_ | channels is an optional reference to a set of channels belonging to<br />the package specified in the packageName field.<br /><br />A "channel" is a package-author-defined stream of updates for an extension.<br /><br />Each channel in the list must follow the DNS subdomain standard<br />as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters,<br />hyphens (-) or periods (.), start and end with an alphanumeric character,<br />and be no longer than 253 characters. No more than 256 channels can be specified.<br /><br />When specified, it is used to constrain the set of installable bundles and<br />the automated upgrade path. This constraint is an AND operation with the<br />version field. For example:<br /> - Given channel is set to "foo"<br /> - Given version is set to ">=1.0.0, <1.5.0"<br /> - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable<br /> - Automatic upgrades will be constrained to upgrade edges defined by the selected channel<br /><br />When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths.<br /><br />Some examples of valid values are:<br /> - 1.1.x<br /> - alpha<br /> - stable<br /> - stable-v1<br /> - v1-stable<br /> - dev-preview<br /> - preview<br /> - community<br /><br />Some examples of invalid values are:<br /> - -some-channel<br /> - some-channel-<br /> - thisisareallylongchannelnamethatisgreaterthanthemaximumlength<br /> - original_40<br /> - --default-channel<br /><br />[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxItems: 256 <br /> |
102102
| `selector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta)_ | selector is an optional field that can be used<br />to filter the set of ClusterCatalogs used in the bundle<br />selection process.<br /><br />When unspecified, all ClusterCatalogs will be used in<br />the bundle selection process. | | |
103103
| `upgradeConstraintPolicy` _[UpgradeConstraintPolicy](#upgradeconstraintpolicy)_ | upgradeConstraintPolicy is an optional field that controls whether<br />the upgrade path(s) defined in the catalog are enforced for the package<br />referenced in the packageName field.<br /><br />Allowed values are: "CatalogProvided" or "SelfCertified", or omitted.<br /><br />When this field is set to "CatalogProvided", automatic upgrades will only occur<br />when upgrade constraints specified by the package author are met.<br /><br />When this field is set to "SelfCertified", the upgrade constraints specified by<br />the package author are ignored. This allows for upgrades and downgrades to<br />any version of the package. This is considered a dangerous operation as it<br />can lead to unknown and potentially disastrous outcomes, such as data<br />loss. It is assumed that users have independently verified changes when<br />using this option.<br /><br />When this field is omitted, the default value is "CatalogProvided". | CatalogProvided | Enum: [CatalogProvided SelfCertified] <br /> |

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,14 @@ spec:
413413
"0.6.0", which means "only install version 0.6.0 and never
414414
upgrade from this version".
415415
416+
For registry+v1 bundles that include release information in the build
417+
metadata field (e.g., "1.0.0+20230101"), you can pin to an exact version
418+
including the release by specifying the full version string with build
419+
metadata (e.g., "1.0.0+20230101"). This ensures an exact match of both
420+
the semver version and the release. If you specify a version without build
421+
metadata (e.g., "1.0.0"), it will match all bundles with that version
422+
regardless of their release information.
423+
416424
# Basic Comparison Operators
417425
418426
The basic comparison operators and their meanings are:

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ spec:
379379
"0.6.0", which means "only install version 0.6.0 and never
380380
upgrade from this version".
381381
382+
For registry+v1 bundles that include release information in the build
383+
metadata field (e.g., "1.0.0+20230101"), you can pin to an exact version
384+
including the release by specifying the full version string with build
385+
metadata (e.g., "1.0.0+20230101"). This ensures an exact match of both
386+
the semver version and the release. If you specify a version without build
387+
metadata (e.g., "1.0.0"), it will match all bundles with that version
388+
regardless of their release information.
389+
382390
# Basic Comparison Operators
383391
384392
The basic comparison operators and their meanings are:
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package bundle
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
bsemver "github.com/blang/semver/v4"
9+
10+
slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices"
11+
)
12+
13+
// NewLegacyRegistryV1VersionRelease parses a registry+v1 bundle version string and returns
14+
// a VersionRelease. For registry+v1 bundles, the build metadata field of the semver version
15+
// is treated as release information (a semver spec violation maintained for backward compatibility).
16+
// The returned VersionRelease has the build metadata extracted into the Release field, and the
17+
// Version field has its Build metadata cleared.
18+
func NewLegacyRegistryV1VersionRelease(vStr string) (*VersionRelease, error) {
19+
vers, err := bsemver.Parse(vStr)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
rel, err := NewRelease(strings.Join(vers.Build, "."))
25+
if err != nil {
26+
return nil, err
27+
}
28+
vers.Build = nil
29+
30+
return &VersionRelease{
31+
Version: vers,
32+
Release: rel,
33+
}, nil
34+
}
35+
36+
type VersionRelease struct {
37+
Version bsemver.Version
38+
Release Release
39+
}
40+
41+
// Compare compares two VersionRelease values. It returns:
42+
//
43+
// -1 if vr < other
44+
// 0 if vr == other
45+
// +1 if vr > other
46+
//
47+
// Comparison is done first by Version, then by Release if versions are equal.
48+
func (vr *VersionRelease) Compare(other VersionRelease) int {
49+
if vCmp := vr.Version.Compare(other.Version); vCmp != 0 {
50+
return vCmp
51+
}
52+
return vr.Release.Compare(other.Release)
53+
}
54+
55+
func (vr *VersionRelease) AsLegacyRegistryV1Version() bsemver.Version {
56+
return bsemver.Version{
57+
Major: vr.Version.Major,
58+
Minor: vr.Version.Minor,
59+
Patch: vr.Version.Patch,
60+
Pre: vr.Version.Pre,
61+
Build: slicesutil.Map(vr.Release, func(i bsemver.PRVersion) string { return i.String() }),
62+
}
63+
}
64+
65+
type Release []bsemver.PRVersion
66+
67+
// Compare compares two Release values. It returns:
68+
//
69+
// -1 if r < other
70+
// 0 if r == other
71+
// +1 if r > other
72+
//
73+
// Comparison is done segment by segment from left to right. Numeric segments are
74+
// compared numerically, and alphanumeric segments are compared lexically in ASCII
75+
// sort order. A shorter release is considered less than a longer release if all
76+
// corresponding segments are equal.
77+
func (r Release) Compare(other Release) int {
78+
if len(r) == 0 && len(other) > 0 {
79+
return -1
80+
}
81+
if len(other) == 0 && len(r) > 0 {
82+
return 1
83+
}
84+
a := bsemver.Version{Pre: r}
85+
b := bsemver.Version{Pre: other}
86+
return a.Compare(b)
87+
}
88+
89+
// NewRelease parses a release string into a Release. The release string should be
90+
// a dot-separated sequence of non-empty identifiers, where each identifier contains
91+
// only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Numeric identifiers (those
92+
// containing only digits) must not have leading zeros. An empty string returns a nil
93+
// Release. Returns an error if any segment is invalid.
94+
func NewRelease(relStr string) (Release, error) {
95+
if relStr == "" {
96+
return nil, nil
97+
}
98+
99+
var (
100+
segments = strings.Split(relStr, ".")
101+
r = make(Release, 0, len(segments))
102+
errs []error
103+
)
104+
for i, segment := range segments {
105+
prVer, err := bsemver.NewPRVersion(segment)
106+
if err != nil {
107+
errs = append(errs, fmt.Errorf("segment %d: %v", i, err))
108+
continue
109+
}
110+
r = append(r, prVer)
111+
}
112+
if err := errors.Join(errs...); err != nil {
113+
return nil, fmt.Errorf("invalid release %q: %v", relStr, err)
114+
}
115+
return r, nil
116+
}

0 commit comments

Comments
 (0)