@@ -5,16 +5,27 @@ import (
55 "encoding/json"
66 "errors"
77 "fmt"
8+ "strings"
89
910 "github.com/blang/semver/v4"
1011 "golang.org/x/text/cases"
1112 utilerrors "k8s.io/apimachinery/pkg/util/errors"
1213 "k8s.io/apimachinery/pkg/util/sets"
1314
15+ "github.com/operator-framework/operator-registry/alpha/model"
1416 "github.com/operator-framework/operator-registry/alpha/property"
1517 prettyunmarshaler "github.com/operator-framework/operator-registry/pkg/prettyunmarshaler"
18+ "github.com/operator-framework/operator-registry/pkg/registry"
1619)
1720
21+ // Re-export VersionRelease/Release types/functions from model package to make it possible for users to only include this package and avoid import cycles
22+ type (
23+ Release = model.Release
24+ VersionRelease = model.VersionRelease
25+ )
26+
27+ var NewRelease = model .NewRelease
28+
1829const (
1930 SchemaPackage = "olm.package"
2031 SchemaChannel = "olm.channel"
@@ -208,24 +219,52 @@ func (destination *DeclarativeConfig) Merge(src *DeclarativeConfig) {
208219 destination .Deprecations = append (destination .Deprecations , src .Deprecations ... )
209220}
210221
211- type CompositeVersion struct {
212- Version semver.Version
213- Release semver.Version
214- }
215-
216- func (cv * CompositeVersion ) Compare (other * CompositeVersion ) int {
217- if cv .Version .NE (other .Version ) {
218- return cv .Version .Compare (other .Version )
219- }
220- hasrelease := len (cv .Release .Pre ) > 0
221- otherhasrelease := len (other .Release .Pre ) > 0
222- if hasrelease && ! otherhasrelease {
223- return 1
222+ // usesLegacyReleaseVersion returns true if the bundle's CSV contains an olm.substitutesFor annotation.
223+ // It checks three possible sources in order:
224+ // 1. CsvJSON field
225+ // 2. olm.csv.metadata property
226+ // 3. olm.bundle.object property containing a CSV
227+ // Returns false if no substitutesFor annotation is found.
228+ // NB: this can only return true for registry+v1 bundles which always have a CSV
229+ func (b * Bundle ) usesLegacyReleaseVersion () bool {
230+ const substitutesForAnnotationKey = "olm.substitutesFor"
231+
232+ // Path 1: Check CsvJSON field if present
233+ if b .CsvJSON != "" {
234+ var csv registry.ClusterServiceVersion
235+ if err := json .Unmarshal ([]byte (b .CsvJSON ), & csv ); err == nil {
236+ return csv .GetSubstitutesFor () != ""
237+ }
238+ // On error, fall through to check other sources
224239 }
225- if ! hasrelease && otherhasrelease {
226- return - 1
240+
241+ // Path 2 & 3: Check properties
242+ for _ , prop := range b .Properties {
243+ switch prop .Type {
244+ case property .TypeCSVMetadata :
245+ var csvMeta property.CSVMetadata
246+ if err := json .Unmarshal (prop .Value , & csvMeta ); err != nil {
247+ continue
248+ }
249+ if csvMeta .Annotations != nil {
250+ if substitutes , ok := csvMeta .Annotations [substitutesForAnnotationKey ]; ok && substitutes != "" {
251+ return true
252+ }
253+ }
254+
255+ case property .TypeBundleObject :
256+ var bundleObj property.BundleObject
257+ if err := json .Unmarshal (prop .Value , & bundleObj ); err != nil {
258+ continue
259+ }
260+ var csv registry.ClusterServiceVersion
261+ if err := json .Unmarshal (bundleObj .Data , & csv ); err == nil {
262+ return csv .GetSubstitutesFor () != ""
263+ }
264+ }
227265 }
228- return cv .Release .Compare (other .Release )
266+
267+ return false
229268}
230269
231270// order by version, then
@@ -234,44 +273,69 @@ func (b *Bundle) Compare(other *Bundle) int {
234273 if b .Name == other .Name {
235274 return 0
236275 }
237- acv , err := b .CompositeVersion ()
276+ avr , err := b .VersionRelease ()
238277 if err != nil {
239278 return 0
240279 }
241- otherCv , err := other .CompositeVersion ()
280+ otherVr , err := other .VersionRelease ()
242281 if err != nil {
243282 return 0
244283 }
245- return acv .Compare (otherCv )
284+ return avr .Compare (otherVr )
246285}
247286
248- func (b * Bundle ) CompositeVersion () (* CompositeVersion , error ) {
249- props , err := property .Parse (b .Properties )
250- if err != nil {
251- return nil , fmt .Errorf ("parse properties for bundle %q: %v" , b .Name , err )
252- }
253- if len (props .Packages ) != 1 {
254- return nil , fmt .Errorf ("bundle %q must have exactly 1 \" olm.package\" property, found %v" , b .Name , len (props .Packages ))
287+ // constructs a VersionRelease from the olm.package property of the bundle
288+ // this handles the cases where the property is present, missing, or duplicated
289+ // if a release field is present in the property, it is used as-is
290+ // if it is NOT present in the property, but the version field contains build metadata,
291+ // we attempt to convert the build metadata into a release and strip the build metadata from the version.
292+ // This is to support bundles that use the legacy approach of encoding release information in the build metadata field of the version
293+ func (b * Bundle ) VersionRelease () (* VersionRelease , error ) {
294+ var (
295+ vr * VersionRelease
296+ )
297+ // loop over all properties, and do not break if we find a package property, in order to check for duplicates
298+ for _ , prop := range b .Properties {
299+ switch prop .Type {
300+ case property .TypePackage :
301+ var p property.Package
302+
303+ // if we encounter more than one olm.package property, return an error
304+ if vr != nil {
305+ return nil , fmt .Errorf ("must be exactly one property of type %q" , SchemaPackage )
306+ }
307+
308+ if err := json .Unmarshal (prop .Value , & p ); err != nil {
309+ return nil , fmt .Errorf ("unable to unmarshal \" olm.package\" property for bundle %q: %v" , b .Name , err )
310+ }
311+ pv , err := semver .Parse (p .Version )
312+ if err != nil {
313+ return nil , fmt .Errorf ("invalid semver version %q in \" olm.package\" property for bundle %q: %v" , p .Version , b .Name , err )
314+ }
315+ pr , err := NewRelease (p .Release )
316+ if err != nil {
317+ return nil , fmt .Errorf ("invalid release %q in \" olm.package\" property for bundle %q: %v" , p .Release , b .Name , err )
318+ }
319+ vr = & VersionRelease {
320+ Version : pv ,
321+ Release : pr ,
322+ }
323+ }
255324 }
256- v , err := semver .Parse (props .Packages [0 ].Version )
257- if err != nil {
258- return nil , fmt .Errorf ("bundle %q has invalid version %q: %v" , b .Name , props .Packages [0 ].Version , err )
325+ if vr == nil {
326+ return nil , fmt .Errorf ("no \" olm.package\" property found for bundle %q" , b .Name )
259327 }
260328
261- var r semver.Version
262- if props .Packages [0 ].Release != "" {
263- r , err = semver .Parse (fmt .Sprintf ("0.0.0-%s" , props .Packages [0 ].Release ))
329+ // if the bundle's release isn't provided, see if we can use the legacy build metadata release approach to identify a release
330+ // if successful, remove the build metadata from the version. Only attempt for bundles using legacy release versioning.
331+ if len (vr .Release ) == 0 && vr .Version .Build != nil && b .usesLegacyReleaseVersion () {
332+ newrel , err := NewRelease (strings .Join (vr .Version .Build , "." ))
264333 if err != nil {
265- return nil , fmt .Errorf ("error parsing bundle %q release version %q: %v" , b .Name , props .Packages [0 ].Release , err )
266- }
267- // only need to check for build metadata since we are using explicit zero major, minor, and patch versions above
268- if len (r .Build ) != 0 {
269- return nil , fmt .Errorf ("bundle %q release version %q cannot contain build metadata" , b .Name , props .Packages [0 ].Release )
334+ return nil , fmt .Errorf ("unable to convert build metadata to release for bundle %q: %v" , b .Name , err )
270335 }
336+ vr .Release = newrel
337+ vr .Version .Build = nil
271338 }
272339
273- return & CompositeVersion {
274- Version : v ,
275- Release : r ,
276- }, nil
340+ return vr , nil
277341}
0 commit comments