Skip to content

Commit

Permalink
Add sort for bundles
Browse files Browse the repository at this point in the history
Signed-off-by: dtfranz <[email protected]>
  • Loading branch information
dtfranz committed Sep 8, 2023
1 parent 786e785 commit 49b0654
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 0 deletions.
75 changes: 75 additions & 0 deletions internal/catalogmetadata/sort/sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package sort

import (
"strings"

"github.com/operator-framework/operator-controller/internal/catalogmetadata"
)

// ByChannelAndVersion is a sort function that orders bundles by package,
// channel (default channel at the head), and inverse version (higher versions on top).
// If a property does not exist for one of the entities, the one missing the property
// is pushed down; if both entities are missing the same property they are ordered by id.
func ByChannelAndVersion(b1, b2 *catalogmetadata.Bundle) bool {
// first sort package lexical order
pkgOrder := packageOrder(b1, b2)
if pkgOrder != 0 {
return pkgOrder < 0
}

// todo(perdasilva): handle default channel in ordering once it is being exposed by the entity
channelOrder := channelOrder(b1, b2)
if channelOrder != 0 {
return channelOrder < 0
}

// order version from highest to lowest (favor the latest release)
versionOrder := versionOrder(b1, b2)
return versionOrder > 0
}

func compareErrors(err1 error, err2 error) int {
if err1 != nil && err2 == nil {
return 1
}

if err1 == nil && err2 != nil {
return -1
}
return 0
}

func packageOrder(b1, b2 *catalogmetadata.Bundle) int {
name1, err1 := b1.PackageName()
name2, err2 := b2.PackageName()
errComp := compareErrors(err1, err2)
if errComp != 0 {
return errComp
}
return strings.Compare(name1, name2)
}

func channelOrder(b1, b2 *catalogmetadata.Bundle) int {
channelProperties1, err1 := b1.Channel()
channelProperties2, err2 := b2.Channel()
errComp := compareErrors(err1, err2)
if errComp != 0 {
return errComp
}
if channelProperties1.Priority != channelProperties2.Priority {
return channelProperties1.Priority - channelProperties2.Priority
}
return strings.Compare(channelProperties1.ChannelName, channelProperties2.ChannelName)
}

func versionOrder(b1, b2 *catalogmetadata.Bundle) int {
ver1, err1 := b1.Version()
ver2, err2 := b2.Version()
errComp := compareErrors(err1, err2)
if errComp != 0 {
// the sign gets inverted because version is sorted
// from highest to lowest
return -1 * errComp
}
return ver1.Compare(*ver2)
}
96 changes: 96 additions & 0 deletions internal/catalogmetadata/sort/sort_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package sort_test

import (
"encoding/json"
"sort"
"testing"

"github.com/operator-framework/operator-controller/internal/catalogmetadata"

Check failure on line 8 in internal/catalogmetadata/sort/sort_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed with -local github.com/operator-framework/operator-controller (goimports)
metadatasort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort"
"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
"github.com/stretchr/testify/assert"
)

func TestSortByPackage(t *testing.T) {
b1 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-a", "version": "1.0.0"}`)},
}}}
b2 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-b", "version": "1.0.0"}`)},
}}}
b3 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-c", "version": "1.0.0"}`)},
}}}

bundles := []*catalogmetadata.Bundle{&b2, &b3, &b1}
sort.Slice(bundles, func(i, j int) bool {
return metadatasort.ByChannelAndVersion(bundles[i], bundles[j])
})
assert.Equal(t, bundles[0], &b1)
assert.Equal(t, bundles[1], &b2)
assert.Equal(t, bundles[2], &b3)
}

func TestSortByChannelName(t *testing.T) {
b1 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":0}`)},
}}}
b2 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"beta","priority":0}`)},
}}}
b3 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"gamma","priority":0}`)},
}}}

bundles := []*catalogmetadata.Bundle{&b3, &b2, &b1}
sort.Slice(bundles, func(i, j int) bool {
return metadatasort.ByChannelAndVersion(bundles[i], bundles[j])
})
assert.Equal(t, bundles[0], &b1)
assert.Equal(t, bundles[1], &b2)
assert.Equal(t, bundles[2], &b3)
}

func TestSortByChannelPriority(t *testing.T) {
b1 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":0}`)},
}}}
b2 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":1}`)},
}}}
b3 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":2}`)},
}}}

bundles := []*catalogmetadata.Bundle{&b3, &b1, &b2}
sort.Slice(bundles, func(i, j int) bool {
return metadatasort.ByChannelAndVersion(bundles[i], bundles[j])
})
assert.Equal(t, bundles[0], &b1)
assert.Equal(t, bundles[1], &b2)
assert.Equal(t, bundles[2], &b3)
}

func TestSortByVersion(t *testing.T) {
b1 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-a", "version": "1.0.0"}`)},
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":0}`)},
}}}
b2 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-a", "version": "1.0.1"}`)},
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":0}`)},
}}}
b3 := catalogmetadata.Bundle{Bundle: declcfg.Bundle{Properties: []property.Property{
{Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "package-a", "version": "2.0.0"}`)},
{Type: property.TypeChannel, Value: json.RawMessage(`{"channelName":"alpha","priority":0}`)},
}}}

bundles := []*catalogmetadata.Bundle{&b2, &b3, &b1}
sort.Slice(bundles, func(i, j int) bool {
return metadatasort.ByChannelAndVersion(bundles[i], bundles[j])
})
assert.Equal(t, bundles[0], &b3)
assert.Equal(t, bundles[1], &b2)
assert.Equal(t, bundles[2], &b1)
}
28 changes: 28 additions & 0 deletions internal/catalogmetadata/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ type Bundle struct {
propertiesMap map[string]property.Property
bundlePackage *property.Package
semVersion *bsemver.Version
channel *property.Channel
providedGVKs []GVK
requiredGVKs []GVK
}

func (b *Bundle) PackageName() (string, error) {
if err := b.loadPackage(); err != nil {
return "", err
}
return b.bundlePackage.PackageName, nil
}

func (b *Bundle) Version() (*bsemver.Version, error) {
if err := b.loadPackage(); err != nil {
return nil, err
Expand All @@ -64,6 +72,13 @@ func (b *Bundle) RequiredGVKs() ([]GVK, error) {
return b.requiredGVKs, nil
}

func (b *Bundle) Channel() (*property.Channel, error) {
if err := b.loadChannel(); err != nil {
return nil, err
}
return b.channel, nil
}

func (b *Bundle) loadPackage() error {
b.mu.Lock()
defer b.mu.Unlock()
Expand Down Expand Up @@ -109,6 +124,19 @@ func (b *Bundle) loadRequiredGVKs() error {
return nil
}

func (b *Bundle) loadChannel() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.channel == nil {
channel, err := loadFromProps[*property.Channel](b, property.TypeChannel, true)
if err != nil {
return fmt.Errorf("error determining channel for bundle %q: %s", b.Name, err)
}
b.channel = channel
}
return nil
}

func (b *Bundle) propertyByType(propType string) *property.Property {
if b.propertiesMap == nil {
b.propertiesMap = make(map[string]property.Property)
Expand Down

0 comments on commit 49b0654

Please sign in to comment.