Skip to content

Commit e514cfd

Browse files
authored
OPRUN-4548: bundle version increased type safety / serialization support (#1938)
* ensure that new types minimize internal access and maximize safeness Signed-off-by: grokspawn <jordan@nimblewidget.com> * provide serialization support Signed-off-by: grokspawn <jordan@nimblewidget.com> * review comments Signed-off-by: grokspawn <jordan@nimblewidget.com> * more review updates Signed-off-by: grokspawn <jordan@nimblewidget.com> --------- Signed-off-by: grokspawn <jordan@nimblewidget.com>
1 parent b058f57 commit e514cfd

8 files changed

Lines changed: 1268 additions & 58 deletions

File tree

alpha/declcfg/declcfg.go

Lines changed: 105 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
1829
const (
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
}

alpha/declcfg/declcfg_to_model.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,12 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
146146
}
147147

148148
// Parse release version from the package property.
149-
var relver semver.Version
149+
var relver model.Release
150150
if props.Packages[0].Release != "" {
151-
relver, err = semver.Parse(fmt.Sprintf("0.0.0-%s", props.Packages[0].Release))
151+
relver, err = model.NewRelease(props.Packages[0].Release)
152152
if err != nil {
153153
return nil, fmt.Errorf("error parsing bundle %q release version %q: %v", b.Name, props.Packages[0].Release, err)
154154
}
155-
// only need to check for build metadata since we are using explicit zero major, minor, and patch versions above
156-
if len(relver.Build) != 0 {
157-
return nil, fmt.Errorf("bundle %q release version %q cannot contain build metadata", b.Name, props.Packages[0].Release)
158-
}
159155
}
160156

161157
channelDefinedEntries[b.Package] = channelDefinedEntries[b.Package].Delete(b.Name)
@@ -170,7 +166,8 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
170166
mb.Objects = b.Objects
171167
mb.PropertiesP = props
172168
mb.Version = ver
173-
mb.Release = relver
169+
// TODO: Jordan: follow-up will evolve the internal types for more consistent use of VersionRelease
170+
mb.Release = semver.Version{Pre: relver}
174171
}
175172
}
176173
if !found {

alpha/declcfg/declcfg_to_model_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ func TestConvertToModel(t *testing.T) {
444444
},
445445
{
446446
name: "Error/InvalidReleaseVersion",
447-
assertion: hasError(`error parsing bundle "foo.v0.1.0" release version "!!!": Invalid character(s) found in prerelease "!!!"`),
447+
assertion: hasError(`error parsing bundle "foo.v0.1.0" release version "!!!": invalid release "!!!": segment 0: Invalid character(s) found in prerelease "!!!"`),
448448
cfg: DeclarativeConfig{
449449
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
450450
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})},

0 commit comments

Comments
 (0)