Skip to content

Commit c5512c7

Browse files
authoredAug 21, 2024··
model: validate unique bundle versions (#1417)
1 parent 7d58951 commit c5512c7

File tree

7 files changed

+109
-2
lines changed

7 files changed

+109
-2
lines changed
 

‎alpha/declcfg/declcfg_to_model_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"encoding/json"
55
"testing"
66

7+
"github.com/blang/semver/v4"
78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910

11+
"github.com/operator-framework/operator-registry/alpha/model"
1012
"github.com/operator-framework/operator-registry/alpha/property"
1113
)
1214

@@ -450,6 +452,41 @@ func TestConvertToModel(t *testing.T) {
450452
}
451453
}
452454

455+
func TestConvertToModelBundle(t *testing.T) {
456+
cfg := DeclarativeConfig{
457+
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
458+
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
459+
Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
460+
}
461+
m, err := ConvertToModel(cfg)
462+
require.NoError(t, err)
463+
464+
pkg, ok := m["foo"]
465+
require.True(t, ok, "expected package 'foo' to be present")
466+
ch, ok := pkg.Channels["alpha"]
467+
require.True(t, ok, "expected channel 'alpha' to be present")
468+
b, ok := ch.Bundles["foo.v0.1.0"]
469+
require.True(t, ok, "expected bundle 'foo.v0.1.0' to be present")
470+
471+
assert.Equal(t, pkg, b.Package)
472+
assert.Equal(t, ch, b.Channel)
473+
assert.Equal(t, "foo.v0.1.0", b.Name)
474+
assert.Equal(t, "foo-bundle:v0.1.0", b.Image)
475+
assert.Equal(t, "", b.Replaces)
476+
assert.Nil(t, b.Skips)
477+
assert.Equal(t, "", b.SkipRange)
478+
assert.Len(t, b.Properties, 3)
479+
assert.Equal(t, []model.RelatedImage{{Name: "bundle", Image: "foo-bundle:v0.1.0"}}, b.RelatedImages)
480+
assert.Nil(t, b.Deprecation)
481+
assert.Len(t, b.Objects, 2)
482+
assert.NotEmpty(t, b.CsvJSON)
483+
assert.NotNil(t, b.PropertiesP)
484+
assert.Len(t, b.PropertiesP.BundleObjects, 2)
485+
assert.Len(t, b.PropertiesP.Packages, 1)
486+
assert.Equal(t, semver.MustParse("0.1.0"), b.Version)
487+
488+
}
489+
453490
func TestConvertToModelRoundtrip(t *testing.T) {
454491
expected := buildValidDeclarativeConfig(validDeclarativeConfigSpec{IncludeUnrecognized: true, IncludeDeprecations: false}) // TODO: turn on deprecation when we have model-->declcfg conversion
455492

‎alpha/declcfg/helpers_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"sort"
77
"testing"
88

9+
"github.com/blang/semver/v4"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112

@@ -244,6 +245,7 @@ func getBundle(pkg *model.Package, ch *model.Channel, version, replaces string,
244245
getCSVJson(pkg.Name, version),
245246
getCRDJSON(),
246247
},
248+
Version: semver.MustParse(version),
247249
}
248250
}
249251

‎alpha/model/model.go

+35
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/h2non/filetype/matchers"
1212
"github.com/h2non/filetype/types"
1313
svg "github.com/h2non/go-is-svg"
14+
"golang.org/x/exp/maps"
1415
"k8s.io/apimachinery/pkg/util/sets"
1516

1617
"github.com/operator-framework/operator-registry/alpha/property"
@@ -86,6 +87,10 @@ func (m *Package) Validate() error {
8687
}
8788
}
8889

90+
if err := m.validateUniqueBundleVersions(); err != nil {
91+
result.subErrors = append(result.subErrors, err)
92+
}
93+
8994
if m.DefaultChannel != nil && !foundDefault {
9095
result.subErrors = append(result.subErrors, fmt.Errorf("default channel %q not found in channels list", m.DefaultChannel.Name))
9196
}
@@ -97,6 +102,36 @@ func (m *Package) Validate() error {
97102
return result.orNil()
98103
}
99104

105+
func (m *Package) validateUniqueBundleVersions() error {
106+
versionsMap := map[string]semver.Version{}
107+
bundlesWithVersion := map[string]sets.Set[string]{}
108+
for _, ch := range m.Channels {
109+
for _, b := range ch.Bundles {
110+
versionsMap[b.Version.String()] = b.Version
111+
if bundlesWithVersion[b.Version.String()] == nil {
112+
bundlesWithVersion[b.Version.String()] = sets.New[string]()
113+
}
114+
bundlesWithVersion[b.Version.String()].Insert(b.Name)
115+
}
116+
}
117+
118+
versionsSlice := maps.Values(versionsMap)
119+
semver.Sort(versionsSlice)
120+
121+
var errs []error
122+
for _, v := range versionsSlice {
123+
bundles := sets.List(bundlesWithVersion[v.String()])
124+
if len(bundles) > 1 {
125+
errs = append(errs, fmt.Errorf("{%s: [%s]}", v, strings.Join(bundles, ", ")))
126+
}
127+
}
128+
129+
if len(errs) > 0 {
130+
return fmt.Errorf("duplicate versions found in bundles: %v", errs)
131+
}
132+
return nil
133+
}
134+
100135
type Icon struct {
101136
Data []byte `json:"base64data"`
102137
MediaType string `json:"mediatype"`

‎alpha/model/model_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"testing"
77

8+
"github.com/blang/semver/v4"
89
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011

@@ -258,6 +259,25 @@ func TestValidators(t *testing.T) {
258259
},
259260
assertion: hasError("package must contain at least one channel"),
260261
},
262+
{
263+
name: "Package/Error/DuplicateBundleVersions",
264+
v: &Package{
265+
Name: "anakin",
266+
Channels: map[string]*Channel{
267+
"light": {
268+
Package: pkg,
269+
Name: "light",
270+
Bundles: map[string]*Bundle{
271+
"anakin.v0.0.1": {Name: "anakin.v0.0.1", Version: semver.MustParse("0.0.1")},
272+
"anakin.v0.0.2": {Name: "anakin.v0.0.2", Version: semver.MustParse("0.0.1")},
273+
"anakin.v1.0.1": {Name: "anakin.v1.0.1", Version: semver.MustParse("1.0.1")},
274+
"anakin.v1.0.2": {Name: "anakin.v1.0.2", Version: semver.MustParse("1.0.1")},
275+
},
276+
},
277+
},
278+
},
279+
assertion: hasError(`duplicate versions found in bundles: [{0.0.1: [anakin.v0.0.1, anakin.v0.0.2]} {1.0.1: [anakin.v1.0.1, anakin.v1.0.2]}]`),
280+
},
261281
{
262282
name: "Package/Error/NoDefaultChannel",
263283
v: &Package{
@@ -612,6 +632,7 @@ func makePackageChannelBundle() (*Package, *Channel) {
612632
property.MustBuildPackage("anakin", "0.0.1"),
613633
property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
614634
},
635+
Version: semver.MustParse("0.0.1"),
615636
}
616637
bundle2 := &Bundle{
617638
Name: "anakin.v0.0.2",
@@ -621,6 +642,7 @@ func makePackageChannelBundle() (*Package, *Channel) {
621642
property.MustBuildPackage("anakin", "0.0.2"),
622643
property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
623644
},
645+
Version: semver.MustParse("0.0.2"),
624646
}
625647
ch := &Channel{
626648
Name: "light",

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/stretchr/testify v1.9.0
3737
github.com/tidwall/btree v1.7.0
3838
go.etcd.io/bbolt v1.3.10
39+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
3940
golang.org/x/mod v0.20.0
4041
golang.org/x/net v0.28.0
4142
golang.org/x/sync v0.8.0
@@ -172,7 +173,6 @@ require (
172173
go.opentelemetry.io/otel/trace v1.28.0 // indirect
173174
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
174175
golang.org/x/crypto v0.26.0 // indirect
175-
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
176176
golang.org/x/oauth2 v0.22.0 // indirect
177177
golang.org/x/term v0.23.0 // indirect
178178
golang.org/x/time v0.5.0 // indirect

‎pkg/api/api_to_model.go

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"sort"
77

8+
"github.com/blang/semver/v4"
9+
810
"github.com/operator-framework/operator-registry/alpha/model"
911
"github.com/operator-framework/operator-registry/alpha/property"
1012
)
@@ -20,6 +22,11 @@ func ConvertAPIBundleToModelBundle(b *Bundle) (*model.Bundle, error) {
2022
return nil, fmt.Errorf("get related iamges: %v", err)
2123
}
2224

25+
vers, err := semver.Parse(b.Version)
26+
if err != nil {
27+
return nil, fmt.Errorf("parse version %q: %v", b.Version, err)
28+
}
29+
2330
return &model.Bundle{
2431
Name: b.CsvName,
2532
Image: b.BundlePath,
@@ -30,6 +37,7 @@ func ConvertAPIBundleToModelBundle(b *Bundle) (*model.Bundle, error) {
3037
Objects: b.Object,
3138
Properties: bundleProps,
3239
RelatedImages: relatedImages,
40+
Version: vers,
3341
}, nil
3442
}
3543

‎pkg/api/conversion_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"encoding/json"
55
"testing"
66

7-
"github.com/operator-framework/api/pkg/operators/v1alpha1"
7+
"github.com/blang/semver/v4"
88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
1010

11+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
12+
1113
"github.com/operator-framework/operator-registry/alpha/model"
1214
"github.com/operator-framework/operator-registry/alpha/property"
1315
)
@@ -77,6 +79,7 @@ func testModelBundle(t *testing.T) model.Bundle {
7779
Image: "quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b",
7880
},
7981
},
82+
Version: semver.MustParse("0.9.4"),
8083
}
8184
return b
8285
}

0 commit comments

Comments
 (0)
Please sign in to comment.