Skip to content

Commit aa8db09

Browse files
authored
feat(opm): Add semver range support for olm alpha diff (#943)
* feat(opm): Add semver range support for olm alpha diff Currently, opm diff allows an array of versions or bundles for diff operation. This commit will enable a semver range being specified for package or channel (but not both) in diff configuration. The diff operator will return bundles that match the range from channel(s). Signed-off-by: Vu Dinh <[email protected]> * Do not fill upgrade graph for range setting Signed-off-by: Vu Dinh <[email protected]> * Add unit tests for diff with range settings Signed-off-by: Vu Dinh <[email protected]>
1 parent aabc493 commit aa8db09

File tree

4 files changed

+824
-22
lines changed

4 files changed

+824
-22
lines changed

alpha/action/diff.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ type DiffIncludePackage struct {
117117
// are parsed for an upgrade graph.
118118
// Set this field only if the named bundle has no semantic version metadata.
119119
Bundles []string `json:"bundles,omitempty" yaml:"bundles,omitempty"`
120+
// Semver range of versions to include. All channels containing these versions
121+
// are parsed for an upgrade graph. If the channels don't contain these versions,
122+
// they will be ignored. This range can only be used with package exclusively
123+
// and cannot combined with `Range` in `DiffIncludeChannel`.
124+
// Range setting is mutually exclusive with channel versions/bundles/range settings.
125+
Range string `json:"range,omitempty" yaml:"range,omitempty"`
120126
}
121127

122128
// DiffIncludeChannel contains a name (required) and versions (optional)
@@ -129,6 +135,11 @@ type DiffIncludeChannel struct {
129135
// Bundles are bundle names to include.
130136
// Set this field only if the named bundle has no semantic version metadata.
131137
Bundles []string `json:"bundles,omitempty" yaml:"bundles,omitempty"`
138+
// Semver range of versions to include in the channel. If the channel don't contain
139+
// these versions, an error will be raised. This range can only be used with
140+
// channel exclusively and cannot combined with `Range` in `DiffIncludePackage`.
141+
// Range setting is mutually exclusive with Versions and Bundles settings.
142+
Range string `json:"range,omitempty" yaml:"range,omitempty"`
132143
}
133144

134145
// LoadDiffIncludeConfig loads a (YAML or JSON) DiffIncludeConfig from r.
@@ -148,11 +159,33 @@ func LoadDiffIncludeConfig(r io.Reader) (c DiffIncludeConfig, err error) {
148159
errs = append(errs, fmt.Errorf("package at index %v requires a name", pkgI))
149160
continue
150161
}
162+
if pkg.Range != "" && (len(pkg.Versions) != 0 || len(pkg.Bundles) != 0) {
163+
errs = append(errs, fmt.Errorf("package %q contains invalid settings: range and versions and/or bundles are mutually exclusive", pkg.Name))
164+
}
165+
if pkg.Range != "" {
166+
_, err := semver.ParseRange(pkg.Range)
167+
if err != nil {
168+
errs = append(errs, fmt.Errorf("package %q has an invalid version range %s", pkg.Name, pkg.Range))
169+
}
170+
}
151171
for chI, ch := range pkg.Channels {
152172
if ch.Name == "" {
153173
errs = append(errs, fmt.Errorf("package %s: channel at index %v requires a name", pkg.Name, chI))
154174
continue
155175
}
176+
if ch.Range == "" {
177+
continue
178+
}
179+
if ch.Range != "" && (len(ch.Versions) != 0 || len(ch.Bundles) != 0) {
180+
errs = append(errs, fmt.Errorf("package %q: channel %q contains invalid settings: range and versions and/or bundles are mutually exclusive", pkg.Name, ch.Name))
181+
}
182+
if pkg.Range != "" && ch.Range != "" {
183+
errs = append(errs, fmt.Errorf("version range settings in package %q and in channel %q must be mutually exclusive", pkg.Name, ch.Name))
184+
}
185+
_, err := semver.ParseRange(ch.Range)
186+
if err != nil {
187+
errs = append(errs, fmt.Errorf("package %s: channel %q has an invalid version range %s", pkg.Name, ch.Name, pkg.Range))
188+
}
156189
}
157190
}
158191
return c, utilerrors.NewAggregate(errs)
@@ -165,6 +198,9 @@ func convertIncludeConfigToIncluder(c DiffIncludeConfig) (includer declcfg.DiffI
165198
pkg.Name = cpkg.Name
166199
pkg.AllChannels.Versions = cpkg.Versions
167200
pkg.AllChannels.Bundles = cpkg.Bundles
201+
if cpkg.Range != "" {
202+
pkg.Range, _ = semver.ParseRange(cpkg.Range)
203+
}
168204

169205
if len(cpkg.Channels) != 0 {
170206
pkg.Channels = make([]declcfg.DiffIncludeChannel, len(cpkg.Channels))
@@ -173,6 +209,9 @@ func convertIncludeConfigToIncluder(c DiffIncludeConfig) (includer declcfg.DiffI
173209
ch.Name = cch.Name
174210
ch.Versions = cch.Versions
175211
ch.Bundles = cch.Bundles
212+
if cch.Range != "" {
213+
ch.Range, _ = semver.ParseRange(cch.Range)
214+
}
176215
}
177216
}
178217
}

alpha/action/diff_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,77 @@ packages:
311311
`,
312312
assertion: require.Error,
313313
},
314+
{
315+
name: "Fail/InvalidPackageRange",
316+
input: `
317+
{
318+
"packages": [
319+
{
320+
"name": "foo",
321+
"range": "test"
322+
}
323+
]
324+
}`,
325+
assertion: require.Error,
326+
},
327+
{
328+
name: "Fail/InvalidChannelRange",
329+
input: `
330+
{
331+
"packages": [
332+
{
333+
"name": "foo",
334+
"channels": [
335+
{
336+
"name": "stable",
337+
"range": "test"
338+
}
339+
]
340+
}
341+
]
342+
}`,
343+
assertion: require.Error,
344+
},
345+
{
346+
name: "Fail/InvalidRangeSetting/MixedRange&ChannelRange",
347+
input: `
348+
{
349+
"packages": [
350+
{
351+
"name": "foo",
352+
"range": "test",
353+
"channels": [
354+
{
355+
"name": "stable",
356+
"range": "test"
357+
}
358+
]
359+
}
360+
]
361+
}`,
362+
assertion: require.Error,
363+
},
364+
{
365+
name: "Fail/InvalidRangeSetting/MixedRange&OtherVersions",
366+
input: `
367+
{
368+
"packages": [
369+
{
370+
"name": "foo",
371+
"channels": [
372+
{
373+
"name": "stable",
374+
"range": ">0.1.0",
375+
"versions": [
376+
"0.1.0"
377+
]
378+
}
379+
]
380+
}
381+
]
382+
}`,
383+
assertion: require.Error,
384+
},
314385
}
315386

316387
for _, s := range specs {

alpha/declcfg/diff_include.go

Lines changed: 153 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@ type DiffIncludePackage struct {
3131
// Upgrade graphs from all channels in the named package containing a version
3232
// from this field are included.
3333
AllChannels DiffIncludeChannel
34+
// The semver range of bundle versions.
35+
// Package range setting is mutually exclusive with channel range/bundles/version
36+
// settings.
37+
Range semver.Range
3438
}
3539

36-
// DiffIncludeChannel specifies a channel, and optionally bundles and bundle versions to include.
40+
// DiffIncludeChannel specifies a channel, and optionally bundles and bundle versions
41+
// (or version range) to include.
3742
type DiffIncludeChannel struct {
3843
// Name of channel.
3944
Name string
@@ -42,13 +47,83 @@ type DiffIncludeChannel struct {
4247
// Bundles are bundle names to include.
4348
// Set this field only if the named bundle has no semantic version metadata.
4449
Bundles []string
50+
// The semver range of bundle versions.
51+
// Range setting is mutually exclusive with Versions and Bundles settings.
52+
Range semver.Range
53+
}
54+
55+
func (dip DiffIncludePackage) Validate() error {
56+
var errs []error
57+
if dip.Name == "" {
58+
errs = append(errs, fmt.Errorf("missing package name"))
59+
}
60+
61+
var isChannelSet bool
62+
for _, ch := range dip.Channels {
63+
isChannelSet = ch.isChannelSet()
64+
err := ch.Validate()
65+
if err != nil {
66+
errs = append(errs, err)
67+
}
68+
}
69+
70+
isChannelSet = dip.AllChannels.isChannelSet()
71+
if isChannelSet && dip.Range != nil {
72+
errs = append(errs, fmt.Errorf("package range setting is mutually exclusive with channel versions/bundles/range settings"))
73+
}
74+
75+
if len(errs) != 0 {
76+
return fmt.Errorf("invalid DiffIncludePackage config for package %q:\n%v", dip.Name, utilerrors.NewAggregate(errs))
77+
}
78+
return nil
79+
}
80+
81+
// isChannelSet returns true if at least one of Range/Bundles/Versions is set
82+
func (dic DiffIncludeChannel) isChannelSet() bool {
83+
return dic.Range != nil || len(dic.Versions) != 0 || len(dic.Bundles) != 0
84+
}
85+
86+
func (dic DiffIncludeChannel) Validate() error {
87+
var errs []error
88+
if dic.Name == "" {
89+
errs = append(errs, fmt.Errorf("missing channel name"))
90+
}
91+
92+
if dic.Range != nil && (len(dic.Versions) != 0 || len(dic.Bundles) != 0) {
93+
errs = append(errs, fmt.Errorf("Channel %q: range and versions/bundles are mutually exclusive", dic.Name))
94+
}
95+
96+
if len(errs) != 0 {
97+
return fmt.Errorf("invalid DiffIncludeChannel config for channel %q:\n%v", dic.Name, utilerrors.NewAggregate(errs))
98+
}
99+
return nil
100+
}
101+
102+
func (i DiffIncluder) Validate() error {
103+
var errs []error
104+
for _, pkg := range i.Packages {
105+
err := pkg.Validate()
106+
if err != nil {
107+
errs = append(errs, err)
108+
}
109+
}
110+
111+
if len(errs) != 0 {
112+
return fmt.Errorf("invalid DiffIncluder config:\n%v", utilerrors.NewAggregate(errs))
113+
}
114+
return nil
45115
}
46116

47117
// Run adds all packages and channels in DiffIncluder with matching names
48118
// directly, and all versions plus their upgrade graphs to channel heads,
49119
// from newModel to outputModel.
50120
func (i DiffIncluder) Run(newModel, outputModel model.Model) error {
51121
var includeErrs []error
122+
if err := i.Validate(); err != nil {
123+
includeErrs = append(includeErrs, err)
124+
return fmt.Errorf("invalid DiffIncluder config:\n%v", utilerrors.NewAggregate(includeErrs))
125+
}
126+
52127
for _, ipkg := range i.Packages {
53128
pkgLog := i.Logger.WithField("package", ipkg.Name)
54129
includeErrs = append(includeErrs, ipkg.includeNewInOutputModel(newModel, outputModel, pkgLog)...)
@@ -59,7 +134,7 @@ func (i DiffIncluder) Run(newModel, outputModel model.Model) error {
59134
return nil
60135
}
61136

62-
// includeNewInOutputModel adds all packages, channels, and versions (bundles)
137+
// includeNewInOutputModel adds all packages, channels, and range (or versions/bundles)
63138
// specified by ipkg that exist in newModel to outputModel. Any package, channel,
64139
// or version in ipkg not satisfied by newModel is an error.
65140
func (ipkg DiffIncludePackage) includeNewInOutputModel(newModel, outputModel model.Model, logger *logrus.Entry) (ierrs []error) {
@@ -72,26 +147,44 @@ func (ipkg DiffIncludePackage) includeNewInOutputModel(newModel, outputModel mod
72147
pkgLog := logger.WithField("package", newPkg.Name)
73148

74149
// No channels or versions were specified, meaning "include the full package".
75-
if len(ipkg.Channels) == 0 && len(ipkg.AllChannels.Versions) == 0 && len(ipkg.AllChannels.Bundles) == 0 {
150+
if len(ipkg.Channels) == 0 && len(ipkg.AllChannels.Versions) == 0 && len(ipkg.AllChannels.Bundles) == 0 && ipkg.Range == nil {
76151
outputModel[ipkg.Name] = newPkg
77152
return nil
78153
}
79154

80155
outputPkg := copyPackageNoChannels(newPkg)
81156
outputModel[outputPkg.Name] = outputPkg
82-
83-
// Add all channels to ipkg.Channels if bundles or versions were specified to include across all channels.
84-
// skipMissingBundleForChannels's value for a channel will be true IFF at least one version is specified,
85-
// since some other channel may contain that version.
86157
skipMissingBundleForChannels := map[string]bool{}
87-
if len(ipkg.AllChannels.Versions) != 0 || len(ipkg.AllChannels.Bundles) != 0 {
88-
for newChName := range newPkg.Channels {
89-
ipkg.Channels = append(ipkg.Channels, DiffIncludeChannel{
90-
Name: newChName,
91-
Versions: ipkg.AllChannels.Versions,
92-
Bundles: ipkg.AllChannels.Bundles,
93-
})
94-
skipMissingBundleForChannels[newChName] = true
158+
if ipkg.Range != nil {
159+
if len(ipkg.Channels) != 0 {
160+
for _, ich := range ipkg.Channels {
161+
if ich.Range != nil {
162+
ierrs = append(ierrs, fmt.Errorf("[package=%q channel=%q] range setting is mutually exclusive between package and channel", newPkg.Name, ich.Name))
163+
}
164+
}
165+
} else {
166+
// Add package range setting to all existing channels if there is no
167+
// channel setting in the config
168+
for newChName := range newPkg.Channels {
169+
ipkg.Channels = append(ipkg.Channels, DiffIncludeChannel{
170+
Name: newChName,
171+
Range: ipkg.Range,
172+
})
173+
}
174+
}
175+
} else {
176+
// Add all channels to ipkg.Channels if bundles or versions were specified to include across all channels.
177+
// skipMissingBundleForChannels's value for a channel will be true IFF at least one version is specified,
178+
// since some other channel may contain that version.
179+
if len(ipkg.AllChannels.Versions) != 0 || len(ipkg.AllChannels.Bundles) != 0 {
180+
for newChName := range newPkg.Channels {
181+
ipkg.Channels = append(ipkg.Channels, DiffIncludeChannel{
182+
Name: newChName,
183+
Versions: ipkg.AllChannels.Versions,
184+
Bundles: ipkg.AllChannels.Bundles,
185+
})
186+
skipMissingBundleForChannels[newChName] = true
187+
}
95188
}
96189
}
97190

@@ -103,7 +196,13 @@ func (ipkg DiffIncludePackage) includeNewInOutputModel(newModel, outputModel mod
103196
}
104197
chLog := pkgLog.WithField("channel", newCh.Name)
105198

106-
bundles, err := getBundlesForVersions(newCh, ich.Versions, ich.Bundles, chLog, skipMissingBundleForChannels[newCh.Name])
199+
var bundles []*model.Bundle
200+
var err error
201+
if ich.Range != nil {
202+
bundles, err = getBundlesForRange(newCh, ich.Range, chLog)
203+
} else {
204+
bundles, err = getBundlesForVersions(newCh, ich.Versions, ich.Bundles, chLog, skipMissingBundleForChannels[newCh.Name])
205+
}
107206
if err != nil {
108207
ierrs = append(ierrs, fmt.Errorf("[package=%q channel=%q] %v", newPkg.Name, newCh.Name, err))
109208
continue
@@ -173,11 +272,43 @@ func getBundlesForVersions(ch *model.Channel, vers []semver.Version, names []str
173272
return nil, fmt.Errorf("bundles do not exist in channel: %s", strings.TrimSpace(sb.String()))
174273
}
175274

176-
// Fill in the upgrade graph between each bundle and head.
177-
// Regardless of semver order, this step needs to be performed
178-
// for each included bundle because there might be leaf nodes
179-
// in the upgrade graph for a particular version not captured
180-
// by any other fill due to skips not being honored here.
275+
bundles, err = fillUpgradeGraph(ch, bundles, logger)
276+
if err != nil {
277+
return nil, err
278+
}
279+
return bundles, nil
280+
}
281+
282+
// getBundlesForRange returns all bundles matching the version range in vers
283+
// If the range is nil, return all bundles in the channel
284+
func getBundlesForRange(ch *model.Channel, vers semver.Range, logger *logrus.Entry) (bundles []*model.Bundle, err error) {
285+
// Short circuit when an empty range was specified, meaning "include the whole channel"
286+
if vers == nil {
287+
for _, b := range ch.Bundles {
288+
bundles = append(bundles, b)
289+
}
290+
return bundles, nil
291+
}
292+
293+
for _, b := range ch.Bundles {
294+
v, err := semver.Parse(b.Version.String())
295+
if err != nil {
296+
return nil, fmt.Errorf("unable to parse bunble version: %s", err.Error())
297+
}
298+
if vers(v) {
299+
bundles = append(bundles, b)
300+
}
301+
}
302+
303+
return bundles, nil
304+
}
305+
306+
// fillUpgradeGraph fills in the upgrade graph between each bundle and head.
307+
// Regardless of semver order, this step needs to be performed
308+
// for each included bundle because there might be leaf nodes
309+
// in the upgrade graph for a particular version not captured
310+
// by any other fill due to skips not being honored here.
311+
func fillUpgradeGraph(ch *model.Channel, bundles []*model.Bundle, logger *logrus.Entry) (bd []*model.Bundle, err error) {
181312
head, err := ch.Head()
182313
if err != nil {
183314
return nil, err
@@ -199,10 +330,10 @@ func getBundlesForVersions(ch *model.Channel, vers []semver.Version, names []str
199330
bundleSet[rb.Name] = rb
200331
}
201332
}
333+
202334
for _, b := range bundleSet {
203335
bundles = append(bundles, b)
204336
}
205-
206337
return bundles, nil
207338
}
208339

0 commit comments

Comments
 (0)