Skip to content

Commit b4557d6

Browse files
committed
add entity sort functions
Signed-off-by: perdasilva <[email protected]>
1 parent a1fa8cd commit b4557d6

File tree

3 files changed

+243
-9
lines changed

3 files changed

+243
-9
lines changed

internal/resolution/variable_sources/entity/bundle_entity.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,18 @@ func (b *BundleEntity) BundlePath() (string, error) {
114114
}
115115

116116
func (b *BundleEntity) loadPackage() error {
117+
b.mu.Lock()
118+
defer b.mu.Unlock()
117119
if b.bundlePackage == nil {
118120
bundlePackage, err := loadFromEntity[property.Package](b.Entity, property.TypePackage)
119121
if err != nil {
120-
return fmt.Errorf("error determining package for entity '%s': %s", b.ID, err)
122+
return fmt.Errorf("error determining package for entity '%s': %w", b.ID, err)
121123
}
122124
b.bundlePackage = &bundlePackage
123125
if b.semVersion == nil {
124126
semVer, err := semver.Parse(b.bundlePackage.Version)
125127
if err != nil {
126-
return fmt.Errorf("could not parse semver (%s) for entity '%s': %s", b.bundlePackage.Version, b.ID, err)
128+
return fmt.Errorf("could not parse semver (%s) for entity '%s': %w", b.bundlePackage.Version, b.ID, err)
127129
}
128130
b.semVersion = &semVer
129131
}
@@ -137,7 +139,7 @@ func (b *BundleEntity) loadProvidedGVKs() error {
137139
if b.providedGVKs == nil {
138140
providedGVKs, err := loadFromEntity[[]GVK](b.Entity, property.TypeGVK)
139141
if err != nil {
140-
return fmt.Errorf("error determining bundle provided gvks for entity '%s': %s", b.ID, err)
142+
return fmt.Errorf("error determining bundle provided gvks for entity '%s': %w", b.ID, err)
141143
}
142144
b.providedGVKs = providedGVKs
143145
}
@@ -150,7 +152,7 @@ func (b *BundleEntity) loadRequiredGVKs() error {
150152
if b.requiredGVKs == nil {
151153
requiredGVKs, err := loadFromEntity[[]GVKRequired](b.Entity, property.TypeGVKRequired)
152154
if err != nil {
153-
return fmt.Errorf("error determining bundle required gvks for entity '%s': %s", b.ID, err)
155+
return fmt.Errorf("error determining bundle required gvks for entity '%s': %w", b.ID, err)
154156
}
155157
b.requiredGVKs = requiredGVKs
156158
}
@@ -163,12 +165,12 @@ func (b *BundleEntity) loadRequiredPackages() error {
163165
if b.requiredPackages == nil {
164166
requiredPackages, err := loadFromEntity[[]PackageRequired](b.Entity, property.TypePackageRequired)
165167
if err != nil {
166-
return fmt.Errorf("error determining bundle required packages for entity '%s': %s", b.ID, err)
168+
return fmt.Errorf("error determining bundle required packages for entity '%s': %w", b.ID, err)
167169
}
168170
for _, requiredPackage := range requiredPackages {
169171
semverRange, err := semver.ParseRange(requiredPackage.VersionRange)
170172
if err != nil {
171-
return fmt.Errorf("error determining bundle required package semver range for entity '%s': '%s'", b.ID, err)
173+
return fmt.Errorf("error determining bundle required package semver range for entity '%s': '%w'", b.ID, err)
172174
}
173175
requiredPackage.SemverRange = &semverRange
174176
}
@@ -183,7 +185,7 @@ func (b *BundleEntity) loadChannelProperties() error {
183185
if b.channelProperties == nil {
184186
channel, err := loadFromEntity[ChannelProperties](b.Entity, property.TypeChannel)
185187
if err != nil {
186-
return fmt.Errorf("error determining bundle channel properties for entity '%s': %s", b.ID, err)
188+
return fmt.Errorf("error determining bundle channel properties for entity '%s': %w", b.ID, err)
187189
}
188190
b.channelProperties = &channel
189191
}
@@ -196,7 +198,7 @@ func (b *BundleEntity) loadBundlePath() error {
196198
if b.bundlePath == "" {
197199
bundlePath, err := loadFromEntity[string](b.Entity, PropertyBundlePath)
198200
if err != nil {
199-
return fmt.Errorf("error determining bundle path for entity '%s': %s", b.ID, err)
201+
return fmt.Errorf("error determining bundle path for entity '%s': %w", b.ID, err)
200202
}
201203
b.bundlePath = bundlePath
202204
}
@@ -211,7 +213,7 @@ func loadFromEntity[T interface{}](entity *input.Entity, propertyName string) (T
211213
}
212214

213215
if err := json.Unmarshal([]byte(propertyValue), &deserializedProperty); err != nil {
214-
return deserializedProperty, fmt.Errorf("property '%s' ('%s') could not be parsed: %s", propertyName, propertyValue, err)
216+
return deserializedProperty, fmt.Errorf("property '%s' ('%s') could not be parsed: %w", propertyName, propertyValue, err)
215217
}
216218
return deserializedProperty, nil
217219
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package sort
2+
3+
import (
4+
"strings"
5+
6+
"github.com/operator-framework/deppy/pkg/deppy/input"
7+
8+
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/entity"
9+
)
10+
11+
// ByChannelAndVersion is an entity sort function that orders the entities in
12+
// package, channel (default channel at the head), and inverse version (higher versions on top)
13+
// if a property does not exist for one of the entities, the one missing the property is pushed down
14+
// if both entities are missing the same property they are ordered by id
15+
func ByChannelAndVersion(entity1 *input.Entity, entity2 *input.Entity) bool {
16+
e1 := entity.NewBundleEntity(entity1)
17+
e2 := entity.NewBundleEntity(entity2)
18+
19+
// first sort package lexical order
20+
pkgOrder := packageOrder(e1, e2)
21+
if pkgOrder != 0 {
22+
return pkgOrder < 0
23+
}
24+
25+
// todo(perdasilva): handle default channel in ordering once it is being exposed by the entity
26+
channelOrder := channelOrder(e1, e2)
27+
if channelOrder != 0 {
28+
return channelOrder < 0
29+
}
30+
31+
// order version from highest to lowest (favor the latest release)
32+
versionOrder := versionOrder(e1, e2)
33+
return versionOrder > 0
34+
}
35+
36+
func compareErrors(err1 error, err2 error) int {
37+
if err1 != nil && err2 == nil {
38+
return 1
39+
}
40+
41+
if err1 == nil && err2 != nil {
42+
return -1
43+
}
44+
return 0
45+
}
46+
47+
func packageOrder(e1, e2 *entity.BundleEntity) int {
48+
name1, err1 := e1.PackageName()
49+
name2, err2 := e2.PackageName()
50+
errComp := compareErrors(err1, err2)
51+
if errComp != 0 {
52+
return errComp
53+
}
54+
return strings.Compare(name1, name2)
55+
}
56+
57+
func channelOrder(e1, e2 *entity.BundleEntity) int {
58+
channelProperties1, err1 := e1.ChannelProperties()
59+
channelProperties2, err2 := e2.ChannelProperties()
60+
errComp := compareErrors(err1, err2)
61+
if errComp != 0 {
62+
return errComp
63+
}
64+
if channelProperties1.Priority != channelProperties2.Priority {
65+
return channelProperties1.Priority - channelProperties2.Priority
66+
}
67+
return strings.Compare(channelProperties1.ChannelName, channelProperties2.ChannelName)
68+
}
69+
70+
func versionOrder(e1, e2 *entity.BundleEntity) int {
71+
ver1, err1 := e1.Version()
72+
ver2, err2 := e2.Version()
73+
errComp := compareErrors(err1, err2)
74+
if errComp != 0 {
75+
// the sign gets inverted because version is sorted
76+
// from highest to lowest
77+
return -1 * errComp
78+
}
79+
return ver1.Compare(*ver2)
80+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package sort_test
2+
3+
import (
4+
"sort"
5+
"testing"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
"github.com/operator-framework/deppy/pkg/deppy/input"
10+
"github.com/operator-framework/operator-registry/alpha/property"
11+
12+
entitysort "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/utils/sort"
13+
)
14+
15+
func TestSort(t *testing.T) {
16+
RegisterFailHandler(Fail)
17+
RunSpecs(t, "Sort Suite")
18+
}
19+
20+
var _ = Describe("Sort", func() {
21+
Describe("ByChannelAndVersion", func() {
22+
It("should order entities by package name", func() {
23+
e1 := input.NewEntity("test1", map[string]string{
24+
property.TypePackage: `{"packageName": "package1", "version": "1.0.0"}`,
25+
})
26+
e2 := input.NewEntity("test2", map[string]string{
27+
property.TypePackage: `{"packageName": "package2", "version": "1.0.0"}`,
28+
})
29+
e3 := input.NewEntity("test3", map[string]string{
30+
property.TypePackage: `{"packageName": "package3", "version": "1.0.0"}`,
31+
})
32+
entities := []*input.Entity{e2, e3, e1}
33+
34+
sort.Slice(entities, func(i, j int) bool {
35+
return entitysort.ByChannelAndVersion(entities[i], entities[j])
36+
})
37+
38+
Expect(entities[0]).To(Equal(e1))
39+
Expect(entities[1]).To(Equal(e2))
40+
Expect(entities[2]).To(Equal(e3))
41+
})
42+
43+
It("should order entities by channel name", func() {
44+
e1 := input.NewEntity("test1", map[string]string{
45+
property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`,
46+
property.TypeChannel: `{"channelName":"stableA","priority":0}`,
47+
})
48+
e2 := input.NewEntity("test2", map[string]string{
49+
property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`,
50+
property.TypeChannel: `{"channelName":"stableB","priority":0}`,
51+
})
52+
e3 := input.NewEntity("test3", map[string]string{
53+
property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`,
54+
property.TypeChannel: `{"channelName":"stableC","priority":0}`,
55+
})
56+
entities := []*input.Entity{e2, e3, e1}
57+
58+
sort.Slice(entities, func(i, j int) bool {
59+
return entitysort.ByChannelAndVersion(entities[i], entities[j])
60+
})
61+
62+
Expect(entities[0]).To(Equal(e1))
63+
Expect(entities[1]).To(Equal(e2))
64+
Expect(entities[2]).To(Equal(e3))
65+
})
66+
67+
It("should order entities by version number (highest first)", func() {
68+
e1 := input.NewEntity("test1", map[string]string{
69+
property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`,
70+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
71+
})
72+
e2 := input.NewEntity("test2", map[string]string{
73+
property.TypePackage: `{"packageName": "mypackage", "version": "2.0.0"}`,
74+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
75+
})
76+
e3 := input.NewEntity("test3", map[string]string{
77+
property.TypePackage: `{"packageName": "mypackage", "version": "3.0.0"}`,
78+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
79+
})
80+
entities := []*input.Entity{e2, e3, e1}
81+
82+
sort.Slice(entities, func(i, j int) bool {
83+
return entitysort.ByChannelAndVersion(entities[i], entities[j])
84+
})
85+
86+
Expect(entities[0]).To(Equal(e3))
87+
Expect(entities[1]).To(Equal(e2))
88+
Expect(entities[2]).To(Equal(e1))
89+
})
90+
91+
It("should order entities by version number (highest first) and channel priority (lower value -> higher priority)", func() {
92+
e1 := input.NewEntity("test1", map[string]string{
93+
property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`,
94+
property.TypeChannel: `{"channelName":"beta","priority":1}`,
95+
})
96+
e2 := input.NewEntity("test2", map[string]string{
97+
property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`,
98+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
99+
})
100+
e3 := input.NewEntity("test3", map[string]string{
101+
property.TypePackage: `{"packageName": "mypackage", "version": "2.0.0"}`,
102+
property.TypeChannel: `{"channelName":"beta","priority":1}`,
103+
})
104+
e4 := input.NewEntity("test4", map[string]string{
105+
property.TypePackage: `{"packageName": "mypackage", "version": "3.0.0"}`,
106+
property.TypeChannel: `{"channelName":"beta","priority":1}`,
107+
})
108+
entities := []*input.Entity{e2, e3, e1, e4}
109+
110+
sort.Slice(entities, func(i, j int) bool {
111+
return entitysort.ByChannelAndVersion(entities[i], entities[j])
112+
})
113+
114+
Expect(entities[0]).To(Equal(e2))
115+
Expect(entities[1]).To(Equal(e4))
116+
Expect(entities[2]).To(Equal(e3))
117+
Expect(entities[3]).To(Equal(e1))
118+
})
119+
120+
It("should order entities missing a property after those that have it", func() {
121+
e1 := input.NewEntity("test1", map[string]string{
122+
property.TypePackage: `{"packageName": "mypackageA", "version": "1.0.0"}`,
123+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
124+
})
125+
e2 := input.NewEntity("test2", map[string]string{
126+
property.TypePackage: `{"packageName": "mypackageA"}`,
127+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
128+
})
129+
e3 := input.NewEntity("test3", map[string]string{
130+
property.TypePackage: `{"packageName": "mypackageA", "version": "3.0.0"}`,
131+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
132+
})
133+
e4 := input.NewEntity("test4", map[string]string{
134+
property.TypePackage: `{"packageName": "mypackageB", "version": "3.0.0"}`,
135+
property.TypeChannel: `{"channelName":"stable","priority":0}`,
136+
})
137+
e5 := input.NewEntity("test5", map[string]string{})
138+
entities := []*input.Entity{e2, e3, e1, e4, e5}
139+
140+
sort.Slice(entities, func(i, j int) bool {
141+
return entitysort.ByChannelAndVersion(entities[i], entities[j])
142+
})
143+
144+
Expect(entities[0]).To(Equal(e3))
145+
Expect(entities[1]).To(Equal(e1))
146+
Expect(entities[2]).To(Equal(e4))
147+
Expect(entities[3]).To(Equal(e2)) // no version
148+
Expect(entities[4]).To(Equal(e5)) // no package - or anything
149+
})
150+
})
151+
152+
})

0 commit comments

Comments
 (0)