From 0928dba4ae0278a5f274009b19c17b65d10ca623 Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Mon, 4 Sep 2023 17:10:02 +0100 Subject: [PATCH 1/5] Adds new `catalogmetadata` package At the moment it contains: * Types which embed `declcfg.Package`, `declcfg.Channel` and `declcfg.Bundle`. Later we will be adding more methods to them (for unmarshalling properties, for example) * A function to fetch metadata by scheme & catalog name * `Filter` function which can filter all these 3 types using a given predicate * `And`, `Or` and `Not` composite predicates to be used with `Filter` Later we might expand/change this package to be more convinient in use with variable sources. Signed-off-by: Mikalai Radchuk --- internal/catalogmetadata/filter.go | 43 ++++++++ internal/catalogmetadata/filter_test.go | 78 +++++++++++++++ internal/catalogmetadata/types.go | 21 ++++ internal/catalogmetadata/unmarshal.go | 20 ++++ internal/catalogmetadata/unmarshal_test.go | 110 +++++++++++++++++++++ 5 files changed, 272 insertions(+) create mode 100644 internal/catalogmetadata/filter.go create mode 100644 internal/catalogmetadata/filter_test.go create mode 100644 internal/catalogmetadata/types.go create mode 100644 internal/catalogmetadata/unmarshal.go create mode 100644 internal/catalogmetadata/unmarshal_test.go diff --git a/internal/catalogmetadata/filter.go b/internal/catalogmetadata/filter.go new file mode 100644 index 000000000..1e23495c1 --- /dev/null +++ b/internal/catalogmetadata/filter.go @@ -0,0 +1,43 @@ +package catalogmetadata + +// Predicate returns true if the object should be kept when filtering +type Predicate[T Schemas] func(entity *T) bool + +// Filter filters a slice accordingly to +func Filter[T Schemas](in []*T, test Predicate[T]) []*T { + out := []*T{} + for i := range in { + if test(in[i]) { + out = append(out, in[i]) + } + } + return out +} + +func And[T Schemas](predicates ...Predicate[T]) Predicate[T] { + return func(obj *T) bool { + for _, predicate := range predicates { + if !predicate(obj) { + return false + } + } + return true + } +} + +func Or[T Schemas](predicates ...Predicate[T]) Predicate[T] { + return func(obj *T) bool { + for _, predicate := range predicates { + if predicate(obj) { + return true + } + } + return false + } +} + +func Not[T Schemas](predicate Predicate[T]) Predicate[T] { + return func(obj *T) bool { + return !predicate(obj) + } +} diff --git a/internal/catalogmetadata/filter_test.go b/internal/catalogmetadata/filter_test.go new file mode 100644 index 000000000..f50b4c6ca --- /dev/null +++ b/internal/catalogmetadata/filter_test.go @@ -0,0 +1,78 @@ +package catalogmetadata_test + +import ( + "testing" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +func TestFilter(t *testing.T) { + in := []*catalogmetadata.Bundle{ + {Bundle: declcfg.Bundle{Name: "operator1.v1", Package: "operator1", Image: "fake1"}}, + {Bundle: declcfg.Bundle{Name: "operator1.v2", Package: "operator1", Image: "fake2"}}, + {Bundle: declcfg.Bundle{Name: "operator2.v1", Package: "operator2", Image: "fake1"}}, + } + + for _, tt := range []struct { + name string + predicate catalogmetadata.Predicate[catalogmetadata.Bundle] + want []*catalogmetadata.Bundle + }{ + { + name: "simple filter with one predicate", + predicate: func(bundle *catalogmetadata.Bundle) bool { + return bundle.Name == "operator1.v1" + }, + want: []*catalogmetadata.Bundle{ + {Bundle: declcfg.Bundle{Name: "operator1.v1", Package: "operator1", Image: "fake1"}}, + }, + }, + { + name: "filter with Not predicate", + predicate: catalogmetadata.Not(func(bundle *catalogmetadata.Bundle) bool { + return bundle.Name == "operator1.v1" + }), + want: []*catalogmetadata.Bundle{ + {Bundle: declcfg.Bundle{Name: "operator1.v2", Package: "operator1", Image: "fake2"}}, + {Bundle: declcfg.Bundle{Name: "operator2.v1", Package: "operator2", Image: "fake1"}}, + }, + }, + { + name: "filter with And predicate", + predicate: catalogmetadata.And( + func(bundle *catalogmetadata.Bundle) bool { + return bundle.Name == "operator1.v1" + }, + func(bundle *catalogmetadata.Bundle) bool { + return bundle.Image == "fake1" + }, + ), + want: []*catalogmetadata.Bundle{ + {Bundle: declcfg.Bundle{Name: "operator1.v1", Package: "operator1", Image: "fake1"}}, + }, + }, + { + name: "filter with Or predicate", + predicate: catalogmetadata.Or( + func(bundle *catalogmetadata.Bundle) bool { + return bundle.Name == "operator1.v1" + }, + func(bundle *catalogmetadata.Bundle) bool { + return bundle.Image == "fake1" + }, + ), + want: []*catalogmetadata.Bundle{ + {Bundle: declcfg.Bundle{Name: "operator1.v1", Package: "operator1", Image: "fake1"}}, + {Bundle: declcfg.Bundle{Name: "operator2.v1", Package: "operator2", Image: "fake1"}}, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + actual := catalogmetadata.Filter(in, tt.predicate) + assert.Equal(t, tt.want, actual) + }) + } +} diff --git a/internal/catalogmetadata/types.go b/internal/catalogmetadata/types.go new file mode 100644 index 000000000..adb225305 --- /dev/null +++ b/internal/catalogmetadata/types.go @@ -0,0 +1,21 @@ +package catalogmetadata + +import ( + "github.com/operator-framework/operator-registry/alpha/declcfg" +) + +type Schemas interface { + Package | Bundle | Channel +} + +type Package struct { + declcfg.Package +} + +type Channel struct { + declcfg.Channel +} + +type Bundle struct { + declcfg.Bundle +} diff --git a/internal/catalogmetadata/unmarshal.go b/internal/catalogmetadata/unmarshal.go new file mode 100644 index 000000000..4bb85b20b --- /dev/null +++ b/internal/catalogmetadata/unmarshal.go @@ -0,0 +1,20 @@ +package catalogmetadata + +import ( + "encoding/json" + + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" +) + +func Unmarshal[T Schemas](cm []catalogd.CatalogMetadata) ([]*T, error) { + contents := make([]*T, 0, len(cm)) + for _, cm := range cm { + var content T + if err := json.Unmarshal(cm.Spec.Content, &content); err != nil { + return nil, err + } + contents = append(contents, &content) + } + + return contents, nil +} diff --git a/internal/catalogmetadata/unmarshal_test.go b/internal/catalogmetadata/unmarshal_test.go new file mode 100644 index 000000000..8ca44acab --- /dev/null +++ b/internal/catalogmetadata/unmarshal_test.go @@ -0,0 +1,110 @@ +package catalogmetadata_test + +import ( + "encoding/json" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +var ( + scheme *runtime.Scheme +) + +func init() { + scheme = runtime.NewScheme() + utilruntime.Must(catalogd.AddToScheme(scheme)) +} + +func TestFetchByScheme(t *testing.T) { + fakeCatalogName := "fake-catalog" + + validBundle := `{ + "schema": "olm.bundle", + "name": "fake1.v1.0.0", + "package": "fake1", + "image": "fake-image", + "properties": [ + { + "type": "olm.package", + "value": {"packageName":"fake1","version":"1.0.0"} + } + ] + }` + + for _, tt := range []struct { + name string + objs []catalogd.CatalogMetadata + wantData []*catalogmetadata.Bundle + wantErr string + }{ + { + name: "valid objects", + objs: []catalogd.CatalogMetadata{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "obj-1", + Labels: map[string]string{"schema": declcfg.SchemaBundle, "catalog": fakeCatalogName}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(validBundle), + }, + }, + }, + wantData: []*catalogmetadata.Bundle{ + { + Bundle: declcfg.Bundle{ + Schema: declcfg.SchemaBundle, + Name: "fake1.v1.0.0", + Package: "fake1", + Image: "fake-image", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName":"fake1","version":"1.0.0"}`), + }, + }, + }, + }, + }, + }, + { + name: "invalid objects", + objs: []catalogd.CatalogMetadata{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "obj-1", + Labels: map[string]string{"schema": declcfg.SchemaBundle, "catalog": fakeCatalogName}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(`{"name":123123123}`), + }, + }, + }, + wantErr: "json: cannot unmarshal number into Go struct field Bundle.name of type string", + }, + { + name: "not found", + wantData: []*catalogmetadata.Bundle{}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + data, err := catalogmetadata.Unmarshal[catalogmetadata.Bundle](tt.objs) + assert.Equal(t, tt.wantData, data) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} From 5bf40f16a1dd428d4c5b2763cac2eb0c51980943 Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Wed, 6 Sep 2023 17:01:13 +0100 Subject: [PATCH 2/5] Adds predicates and tests for them Signed-off-by: Mikalai Radchuk Signed-off-by: dtfranz --- internal/catalogmetadata/predicates.go | 88 +++++++++ internal/catalogmetadata/predicates_test.go | 187 ++++++++++++++++++ internal/catalogmetadata/types.go | 128 ++++++++++++ internal/catalogmetadata/types_test.go | 152 ++++++++++++++ internal/resolution/entities/bundle_entity.go | 4 +- 5 files changed, 557 insertions(+), 2 deletions(-) create mode 100644 internal/catalogmetadata/predicates.go create mode 100644 internal/catalogmetadata/predicates_test.go create mode 100644 internal/catalogmetadata/types_test.go diff --git a/internal/catalogmetadata/predicates.go b/internal/catalogmetadata/predicates.go new file mode 100644 index 000000000..bd85a3cfa --- /dev/null +++ b/internal/catalogmetadata/predicates.go @@ -0,0 +1,88 @@ +package catalogmetadata + +import ( + mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" +) + +// TODO: Move somewhere nice. Probably into a ./predicates package + +func WithPackageName(packageName string) Predicate[Bundle] { + return func(bundle *Bundle) bool { + return bundle.Package == packageName + } +} + +func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[Bundle] { + return func(bundle *Bundle) bool { + bVersion, err := bundle.Version() + if err != nil { + return false + } + // No error should occur here because the simple version was successfully parsed by blang + // We are unaware of any tests cases that would cause one to fail but not the other + // This will cause code coverage to drop for this line. We don't ignore the error because + // there might be that one extreme edge case that might cause one to fail but not the other + mVersion, err := mmsemver.NewVersion(bVersion.String()) + if err != nil { + return false + } + return semverRange.Check(mVersion) + } +} + +func InBlangSemverRange(semverRange bsemver.Range) Predicate[Bundle] { + return func(bundle *Bundle) bool { + bundleVersion, err := bundle.Version() + if err != nil { + return false + } + return semverRange(*bundleVersion) + } +} + +func InChannel(channelName string) Predicate[Bundle] { + return func(bundle *Bundle) bool { + for _, ch := range bundle.InChannels { + if ch.Name == channelName { + return true + } + } + return false + } +} + +func ProvidesGVK(gvk *GVK) Predicate[Bundle] { + return func(bundle *Bundle) bool { + providedGVKs, err := bundle.ProvidedGVKs() + if err != nil { + return false + } + for i := 0; i < len(providedGVKs); i++ { + providedGVK := &providedGVKs[i] + if providedGVK.String() == gvk.String() { + return true + } + } + return false + } +} + +func WithBundleImage(bundleImage string) Predicate[Bundle] { + return func(bundle *Bundle) bool { + return bundle.Image == bundleImage + } +} + +func Replaces(bundleName string) Predicate[Bundle] { + return func(bundle *Bundle) bool { + for _, ch := range bundle.InChannels { + for _, chEntry := range ch.Entries { + if bundle.Name == chEntry.Name && chEntry.Replaces == bundleName { + return true + } + } + } + return false + } +} diff --git a/internal/catalogmetadata/predicates_test.go b/internal/catalogmetadata/predicates_test.go new file mode 100644 index 000000000..ce151b921 --- /dev/null +++ b/internal/catalogmetadata/predicates_test.go @@ -0,0 +1,187 @@ +package catalogmetadata_test + +import ( + "encoding/json" + "testing" + + mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +func TestWithPackageName(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Package: "package1"}} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Package: "package2"}} + b3 := &catalogmetadata.Bundle{} + + f := catalogmetadata.WithPackageName("package1") + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} + +func TestInMastermindsSemverRange(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0"}`), + }, + }, + }} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "0.0.1"}`), + }, + }, + }} + b3 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "broken"}`), + }, + }, + }} + + vRange, err := mmsemver.NewConstraint(">=1.0.0") + assert.NoError(t, err) + + f := catalogmetadata.InMastermindsSemverRange(vRange) + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} + +func TestInBlangSemverRange(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0"}`), + }, + }, + }} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "0.0.1"}`), + }, + }, + }} + b3 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "broken"}`), + }, + }, + }} + + vRange := bsemver.MustParseRange(">=1.0.0") + + f := catalogmetadata.InBlangSemverRange(vRange) + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} + +func TestInChannel(t *testing.T) { + b1 := &catalogmetadata.Bundle{InChannels: []*catalogmetadata.Channel{ + {Channel: declcfg.Channel{Name: "alpha"}}, + {Channel: declcfg.Channel{Name: "stable"}}, + }} + b2 := &catalogmetadata.Bundle{InChannels: []*catalogmetadata.Channel{ + {Channel: declcfg.Channel{Name: "alpha"}}, + }} + b3 := &catalogmetadata.Bundle{} + + f := catalogmetadata.InChannel("stable") + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} + +func TestProvidesGVK(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Properties: []property.Property{ + { + Type: property.TypeGVK, + Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"},{"group":"bar.io","kind":"Bar","version":"v1"}]`), + }, + }, + }} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{}} + f1 := catalogmetadata.ProvidesGVK(&catalogmetadata.GVK{ + Group: "foo.io", + Version: "v1", + Kind: "Foo", + }) + f2 := catalogmetadata.ProvidesGVK(&catalogmetadata.GVK{ + Group: "baz.io", + Version: "v1alpha1", + Kind: "Baz", + }) + // Filter with Bundle which provides the GVK should return true + assert.True(t, f1(b1)) + // Filter with Bundle which does not provide the GVK should return false + assert.False(t, f2(b1)) + // Filter with Bundle with no GVK should return false + assert.False(t, f1(b2)) +} + +func TestWithBundleImage(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Image: "fake-image-uri-1"}} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Image: "fake-image-uri-2"}} + b3 := &catalogmetadata.Bundle{} + + f := catalogmetadata.WithBundleImage("fake-image-uri-1") + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} + +func TestReplaces(t *testing.T) { + fakeChannel := &catalogmetadata.Channel{ + Channel: declcfg.Channel{ + Entries: []declcfg.ChannelEntry{ + { + Name: "package1.v0.0.2", + Replaces: "package1.v0.0.1", + }, + { + Name: "package1.v0.0.3", + Replaces: "package1.v0.0.2", + }, + }, + }, + } + + b1 := &catalogmetadata.Bundle{ + Bundle: declcfg.Bundle{Name: "package1.v0.0.2"}, + InChannels: []*catalogmetadata.Channel{fakeChannel}, + } + b2 := &catalogmetadata.Bundle{ + Bundle: declcfg.Bundle{Name: "package1.v0.0.3"}, + InChannels: []*catalogmetadata.Channel{fakeChannel}, + } + b3 := &catalogmetadata.Bundle{} + + f := catalogmetadata.Replaces("package1.v0.0.1") + + assert.True(t, f(b1)) + assert.False(t, f(b2)) + assert.False(t, f(b3)) +} diff --git a/internal/catalogmetadata/types.go b/internal/catalogmetadata/types.go index adb225305..52d55248c 100644 --- a/internal/catalogmetadata/types.go +++ b/internal/catalogmetadata/types.go @@ -1,7 +1,14 @@ package catalogmetadata import ( + "encoding/json" + "fmt" + "sync" + + bsemver "github.com/blang/semver/v4" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" ) type Schemas interface { @@ -16,6 +23,127 @@ type Channel struct { declcfg.Channel } +type GVK property.GVK + +func (g GVK) String() string { + return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) +} + +type GVKRequired property.GVKRequired + +func (g GVKRequired) String() string { + return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) +} + +func (g GVKRequired) AsGVK() GVK { + return GVK(g) +} + type Bundle struct { declcfg.Bundle + InChannels []*Channel + + mu sync.RWMutex + // these properties are lazy loaded as they are requested + propertiesMap map[string]property.Property + bundlePackage *property.Package + semVersion *bsemver.Version + providedGVKs []GVK + requiredGVKs []GVKRequired +} + +func (b *Bundle) Version() (*bsemver.Version, error) { + if err := b.loadPackage(); err != nil { + return nil, err + } + return b.semVersion, nil +} + +func (b *Bundle) ProvidedGVKs() ([]GVK, error) { + if err := b.loadProvidedGVKs(); err != nil { + return nil, err + } + return b.providedGVKs, nil +} + +func (b *Bundle) RequiredGVKs() ([]GVKRequired, error) { + if err := b.loadRequiredGVKs(); err != nil { + return nil, err + } + return b.requiredGVKs, nil +} + +func (b *Bundle) loadPackage() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.bundlePackage == nil { + bundlePackage, err := loadFromProps[property.Package](b, property.TypePackage, true) + if err != nil { + return err + } + b.bundlePackage = &bundlePackage + } + if b.semVersion == nil { + semVer, err := bsemver.Parse(b.bundlePackage.Version) + if err != nil { + return fmt.Errorf("could not parse semver %q for bundle '%s': %s", b.bundlePackage.Version, b.Name, err) + } + b.semVersion = &semVer + } + return nil +} + +func (b *Bundle) loadProvidedGVKs() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.providedGVKs == nil { + providedGVKs, err := loadFromProps[[]GVK](b, property.TypeGVK, false) + if err != nil { + return fmt.Errorf("error determining provided GVKs for bundle %q: %s", b.Name, err) + } + b.providedGVKs = providedGVKs + } + return nil +} + +func (b *Bundle) loadRequiredGVKs() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.requiredGVKs == nil { + requiredGVKs, err := loadFromProps[[]GVKRequired](b, property.TypeGVKRequired, false) + if err != nil { + return fmt.Errorf("error determining required GVKs for bundle %q: %s", b.Name, err) + } + b.requiredGVKs = requiredGVKs + } + return nil +} + +func (b *Bundle) propertyByType(propType string) *property.Property { + if b.propertiesMap == nil { + b.propertiesMap = make(map[string]property.Property) + for _, prop := range b.Properties { + b.propertiesMap[prop.Type] = prop + } + } + + prop, ok := b.propertiesMap[propType] + if !ok { + return nil + } + return &prop +} + +func loadFromProps[T any](bundle *Bundle, propType string, required bool) (T, error) { + parsedProp := *new(T) + prop := bundle.propertyByType(propType) + if prop != nil { + if err := json.Unmarshal(prop.Value, &parsedProp); err != nil { + return parsedProp, fmt.Errorf("property %q with value %q could not be parsed: %s", propType, prop.Value, err) + } + } else if required { + return parsedProp, fmt.Errorf("bundle property with type %q not found", propType) + } + + return parsedProp, nil } diff --git a/internal/catalogmetadata/types_test.go b/internal/catalogmetadata/types_test.go new file mode 100644 index 000000000..9c6ea631c --- /dev/null +++ b/internal/catalogmetadata/types_test.go @@ -0,0 +1,152 @@ +package catalogmetadata_test + +import ( + "encoding/json" + "testing" + + bsemver "github.com/blang/semver/v4" + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +func TestGVK(t *testing.T) { + t.Run("String", func(t *testing.T) { + gvk := catalogmetadata.GVK{Group: "bar.io", Kind: "Bar", Version: "v1"} + + assert.Equal(t, `group:"bar.io" version:"v1" kind:"Bar"`, gvk.String()) + }) +} + +func TestGVKRequired(t *testing.T) { + t.Run("String", func(t *testing.T) { + gvk := catalogmetadata.GVKRequired{Group: "bar.io", Kind: "Bar", Version: "v1"} + + assert.Equal(t, `group:"bar.io" version:"v1" kind:"Bar"`, gvk.String()) + }) + + t.Run("AsGVK", func(t *testing.T) { + gvk := catalogmetadata.GVKRequired{Group: "bar.io", Kind: "Bar", Version: "v1"} + + assert.Equal(t, catalogmetadata.GVK{Group: "bar.io", Kind: "Bar", Version: "v1"}, gvk.AsGVK()) + }) +} + +func TestBundle(t *testing.T) { + t.Run("Version", func(t *testing.T) { + validVersion := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.v1", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0"}`), + }, + }, + }} + invalidVersion := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.invalid", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "broken"}`), + }, + }, + }} + noVersion := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.noVersion", + }} + + ver, err := validVersion.Version() + assert.NoError(t, err) + assert.Equal(t, &bsemver.Version{Major: 1}, ver) + + ver, err = invalidVersion.Version() + assert.EqualError(t, err, "could not parse semver \"broken\" for bundle 'fake-bundle.invalid': No Major.Minor.Patch elements found") + assert.Nil(t, ver) + + ver, err = noVersion.Version() + assert.EqualError(t, err, "bundle property with type \"olm.package\" not found") + assert.Nil(t, ver) + }) + + t.Run("ProvidedGVKs", func(t *testing.T) { + validGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.v1", + Properties: []property.Property{ + { + Type: property.TypeGVK, + Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"},{"group":"bar.io","kind":"Bar","version":"v1alpha1"}]`), + }, + }, + }} + invalidGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.invalid", + Properties: []property.Property{ + { + Type: property.TypeGVK, + Value: json.RawMessage(`badGvkStructure`), + }, + }, + }} + noGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.noGVK", + }} + + gvk, err := validGVK.ProvidedGVKs() + assert.NoError(t, err) + assert.Equal(t, []catalogmetadata.GVK{ + {Group: "foo.io", Kind: "Foo", Version: "v1"}, + {Group: "bar.io", Kind: "Bar", Version: "v1alpha1"}, + }, gvk) + + gvk, err = invalidGVK.ProvidedGVKs() + assert.EqualError(t, err, "error determining provided GVKs for bundle \"fake-bundle.invalid\": property \"olm.gvk\" with value \"badGvkStructure\" could not be parsed: invalid character 'b' looking for beginning of value") + assert.Nil(t, gvk) + + gvk, err = noGVK.ProvidedGVKs() + assert.NoError(t, err) + assert.Nil(t, gvk) + }) + + t.Run("RequiredGVKs", func(t *testing.T) { + validGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.v1", + Properties: []property.Property{ + { + Type: property.TypeGVKRequired, + Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"},{"group":"bar.io","kind":"Bar","version":"v1alpha1"}]`), + }, + }, + }} + invalidGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.invalid", + Properties: []property.Property{ + { + Type: property.TypeGVKRequired, + Value: json.RawMessage(`badGvkStructure`), + }, + }, + }} + noGVK := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "fake-bundle.noGVK", + }} + + gvk, err := validGVK.RequiredGVKs() + assert.NoError(t, err) + assert.Equal(t, []catalogmetadata.GVKRequired{ + {Group: "foo.io", Kind: "Foo", Version: "v1"}, + {Group: "bar.io", Kind: "Bar", Version: "v1alpha1"}, + }, gvk) + + gvk, err = invalidGVK.RequiredGVKs() + assert.EqualError(t, err, "error determining required GVKs for bundle \"fake-bundle.invalid\": property \"olm.gvk.required\" with value \"badGvkStructure\" could not be parsed: invalid character 'b' looking for beginning of value") + assert.Nil(t, gvk) + + gvk, err = noGVK.RequiredGVKs() + assert.NoError(t, err) + assert.Nil(t, gvk) + }) +} diff --git a/internal/resolution/entities/bundle_entity.go b/internal/resolution/entities/bundle_entity.go index b9ccf4cce..6f28d4a32 100644 --- a/internal/resolution/entities/bundle_entity.go +++ b/internal/resolution/entities/bundle_entity.go @@ -35,14 +35,14 @@ type PackageRequired struct { SemverRange *bsemver.Range `json:"-"` } -type GVK property.GVK - type ChannelEntry struct { Name string `json:"name"` Replaces string `json:"replaces"` // Skips and skipRange will probably go here as well } +type GVK property.GVK + func (g GVK) String() string { return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) } From ea6024122e8685257deae5c3919f6e0d8231511b Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Thu, 7 Sep 2023 12:25:29 +0100 Subject: [PATCH 3/5] Moves filter and predicates into a sub package Signed-off-by: Mikalai Radchuk --- .../bundle_predicates.go} | 34 +++++++++---------- .../bundle_predicates_test.go} | 19 ++++++----- .../catalogmetadata/{ => filter}/filter.go | 16 +++++---- .../{ => filter}/filter_test.go | 13 +++---- 4 files changed, 44 insertions(+), 38 deletions(-) rename internal/catalogmetadata/{predicates.go => filter/bundle_predicates.go} (61%) rename internal/catalogmetadata/{predicates_test.go => filter/bundle_predicates_test.go} (91%) rename internal/catalogmetadata/{ => filter}/filter.go (53%) rename internal/catalogmetadata/{ => filter}/filter_test.go (87%) diff --git a/internal/catalogmetadata/predicates.go b/internal/catalogmetadata/filter/bundle_predicates.go similarity index 61% rename from internal/catalogmetadata/predicates.go rename to internal/catalogmetadata/filter/bundle_predicates.go index bd85a3cfa..9260d4c6c 100644 --- a/internal/catalogmetadata/predicates.go +++ b/internal/catalogmetadata/filter/bundle_predicates.go @@ -1,20 +1,20 @@ -package catalogmetadata +package filter import ( mmsemver "github.com/Masterminds/semver/v3" bsemver "github.com/blang/semver/v4" -) -// TODO: Move somewhere nice. Probably into a ./predicates package + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) -func WithPackageName(packageName string) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func WithPackageName(packageName string) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { return bundle.Package == packageName } } -func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { bVersion, err := bundle.Version() if err != nil { return false @@ -31,8 +31,8 @@ func InMastermindsSemverRange(semverRange *mmsemver.Constraints) Predicate[Bundl } } -func InBlangSemverRange(semverRange bsemver.Range) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func InBlangSemverRange(semverRange bsemver.Range) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { bundleVersion, err := bundle.Version() if err != nil { return false @@ -41,8 +41,8 @@ func InBlangSemverRange(semverRange bsemver.Range) Predicate[Bundle] { } } -func InChannel(channelName string) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func InChannel(channelName string) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { for _, ch := range bundle.InChannels { if ch.Name == channelName { return true @@ -52,8 +52,8 @@ func InChannel(channelName string) Predicate[Bundle] { } } -func ProvidesGVK(gvk *GVK) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func ProvidesGVK(gvk *catalogmetadata.GVK) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { providedGVKs, err := bundle.ProvidedGVKs() if err != nil { return false @@ -68,14 +68,14 @@ func ProvidesGVK(gvk *GVK) Predicate[Bundle] { } } -func WithBundleImage(bundleImage string) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func WithBundleImage(bundleImage string) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { return bundle.Image == bundleImage } } -func Replaces(bundleName string) Predicate[Bundle] { - return func(bundle *Bundle) bool { +func Replaces(bundleName string) Predicate[catalogmetadata.Bundle] { + return func(bundle *catalogmetadata.Bundle) bool { for _, ch := range bundle.InChannels { for _, chEntry := range ch.Entries { if bundle.Name == chEntry.Name && chEntry.Replaces == bundleName { diff --git a/internal/catalogmetadata/predicates_test.go b/internal/catalogmetadata/filter/bundle_predicates_test.go similarity index 91% rename from internal/catalogmetadata/predicates_test.go rename to internal/catalogmetadata/filter/bundle_predicates_test.go index ce151b921..4be55aa88 100644 --- a/internal/catalogmetadata/predicates_test.go +++ b/internal/catalogmetadata/filter/bundle_predicates_test.go @@ -1,4 +1,4 @@ -package catalogmetadata_test +package filter_test import ( "encoding/json" @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/operator-framework/operator-controller/internal/catalogmetadata" + "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" ) func TestWithPackageName(t *testing.T) { @@ -18,7 +19,7 @@ func TestWithPackageName(t *testing.T) { b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Package: "package2"}} b3 := &catalogmetadata.Bundle{} - f := catalogmetadata.WithPackageName("package1") + f := filter.WithPackageName("package1") assert.True(t, f(b1)) assert.False(t, f(b2)) @@ -54,7 +55,7 @@ func TestInMastermindsSemverRange(t *testing.T) { vRange, err := mmsemver.NewConstraint(">=1.0.0") assert.NoError(t, err) - f := catalogmetadata.InMastermindsSemverRange(vRange) + f := filter.InMastermindsSemverRange(vRange) assert.True(t, f(b1)) assert.False(t, f(b2)) @@ -89,7 +90,7 @@ func TestInBlangSemverRange(t *testing.T) { vRange := bsemver.MustParseRange(">=1.0.0") - f := catalogmetadata.InBlangSemverRange(vRange) + f := filter.InBlangSemverRange(vRange) assert.True(t, f(b1)) assert.False(t, f(b2)) @@ -106,7 +107,7 @@ func TestInChannel(t *testing.T) { }} b3 := &catalogmetadata.Bundle{} - f := catalogmetadata.InChannel("stable") + f := filter.InChannel("stable") assert.True(t, f(b1)) assert.False(t, f(b2)) @@ -123,12 +124,12 @@ func TestProvidesGVK(t *testing.T) { }, }} b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{}} - f1 := catalogmetadata.ProvidesGVK(&catalogmetadata.GVK{ + f1 := filter.ProvidesGVK(&catalogmetadata.GVK{ Group: "foo.io", Version: "v1", Kind: "Foo", }) - f2 := catalogmetadata.ProvidesGVK(&catalogmetadata.GVK{ + f2 := filter.ProvidesGVK(&catalogmetadata.GVK{ Group: "baz.io", Version: "v1alpha1", Kind: "Baz", @@ -146,7 +147,7 @@ func TestWithBundleImage(t *testing.T) { b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{Image: "fake-image-uri-2"}} b3 := &catalogmetadata.Bundle{} - f := catalogmetadata.WithBundleImage("fake-image-uri-1") + f := filter.WithBundleImage("fake-image-uri-1") assert.True(t, f(b1)) assert.False(t, f(b2)) @@ -179,7 +180,7 @@ func TestReplaces(t *testing.T) { } b3 := &catalogmetadata.Bundle{} - f := catalogmetadata.Replaces("package1.v0.0.1") + f := filter.Replaces("package1.v0.0.1") assert.True(t, f(b1)) assert.False(t, f(b2)) diff --git a/internal/catalogmetadata/filter.go b/internal/catalogmetadata/filter/filter.go similarity index 53% rename from internal/catalogmetadata/filter.go rename to internal/catalogmetadata/filter/filter.go index 1e23495c1..36af20661 100644 --- a/internal/catalogmetadata/filter.go +++ b/internal/catalogmetadata/filter/filter.go @@ -1,10 +1,14 @@ -package catalogmetadata +package filter + +import ( + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) // Predicate returns true if the object should be kept when filtering -type Predicate[T Schemas] func(entity *T) bool +type Predicate[T catalogmetadata.Schemas] func(entity *T) bool // Filter filters a slice accordingly to -func Filter[T Schemas](in []*T, test Predicate[T]) []*T { +func Filter[T catalogmetadata.Schemas](in []*T, test Predicate[T]) []*T { out := []*T{} for i := range in { if test(in[i]) { @@ -14,7 +18,7 @@ func Filter[T Schemas](in []*T, test Predicate[T]) []*T { return out } -func And[T Schemas](predicates ...Predicate[T]) Predicate[T] { +func And[T catalogmetadata.Schemas](predicates ...Predicate[T]) Predicate[T] { return func(obj *T) bool { for _, predicate := range predicates { if !predicate(obj) { @@ -25,7 +29,7 @@ func And[T Schemas](predicates ...Predicate[T]) Predicate[T] { } } -func Or[T Schemas](predicates ...Predicate[T]) Predicate[T] { +func Or[T catalogmetadata.Schemas](predicates ...Predicate[T]) Predicate[T] { return func(obj *T) bool { for _, predicate := range predicates { if predicate(obj) { @@ -36,7 +40,7 @@ func Or[T Schemas](predicates ...Predicate[T]) Predicate[T] { } } -func Not[T Schemas](predicate Predicate[T]) Predicate[T] { +func Not[T catalogmetadata.Schemas](predicate Predicate[T]) Predicate[T] { return func(obj *T) bool { return !predicate(obj) } diff --git a/internal/catalogmetadata/filter_test.go b/internal/catalogmetadata/filter/filter_test.go similarity index 87% rename from internal/catalogmetadata/filter_test.go rename to internal/catalogmetadata/filter/filter_test.go index f50b4c6ca..942b76fa7 100644 --- a/internal/catalogmetadata/filter_test.go +++ b/internal/catalogmetadata/filter/filter_test.go @@ -1,4 +1,4 @@ -package catalogmetadata_test +package filter_test import ( "testing" @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/operator-framework/operator-controller/internal/catalogmetadata" + "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" ) func TestFilter(t *testing.T) { @@ -18,7 +19,7 @@ func TestFilter(t *testing.T) { for _, tt := range []struct { name string - predicate catalogmetadata.Predicate[catalogmetadata.Bundle] + predicate filter.Predicate[catalogmetadata.Bundle] want []*catalogmetadata.Bundle }{ { @@ -32,7 +33,7 @@ func TestFilter(t *testing.T) { }, { name: "filter with Not predicate", - predicate: catalogmetadata.Not(func(bundle *catalogmetadata.Bundle) bool { + predicate: filter.Not(func(bundle *catalogmetadata.Bundle) bool { return bundle.Name == "operator1.v1" }), want: []*catalogmetadata.Bundle{ @@ -42,7 +43,7 @@ func TestFilter(t *testing.T) { }, { name: "filter with And predicate", - predicate: catalogmetadata.And( + predicate: filter.And( func(bundle *catalogmetadata.Bundle) bool { return bundle.Name == "operator1.v1" }, @@ -56,7 +57,7 @@ func TestFilter(t *testing.T) { }, { name: "filter with Or predicate", - predicate: catalogmetadata.Or( + predicate: filter.Or( func(bundle *catalogmetadata.Bundle) bool { return bundle.Name == "operator1.v1" }, @@ -71,7 +72,7 @@ func TestFilter(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - actual := catalogmetadata.Filter(in, tt.predicate) + actual := filter.Filter(in, tt.predicate) assert.Equal(t, tt.want, actual) }) } From 41914ce07dab077fbd9c0ab849661489039c3157 Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Thu, 7 Sep 2023 15:14:33 +0100 Subject: [PATCH 4/5] Adds client to fetch bundles Signed-off-by: Mikalai Radchuk --- internal/catalogmetadata/client/client.go | 92 +++++ .../catalogmetadata/client/client_test.go | 321 ++++++++++++++++++ internal/catalogmetadata/types.go | 3 +- 3 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 internal/catalogmetadata/client/client.go create mode 100644 internal/catalogmetadata/client/client_test.go diff --git a/internal/catalogmetadata/client/client.go b/internal/catalogmetadata/client/client.go new file mode 100644 index 000000000..b472d14ff --- /dev/null +++ b/internal/catalogmetadata/client/client.go @@ -0,0 +1,92 @@ +package client + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/declcfg" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +func NewClient(cl client.Client) *Client { + return &Client{cl: cl} +} + +// Client is reading catalog metadata +type Client struct { + // Note that eventually we will be reading from catalogd http API + // instead of kube API server. We will need to swap this implementation. + cl client.Client +} + +func (c *Client) Bundles(ctx context.Context) ([]*catalogmetadata.Bundle, error) { + var allBundles []*catalogmetadata.Bundle + + var catalogList catalogd.CatalogList + if err := c.cl.List(ctx, &catalogList); err != nil { + return nil, err + } + for _, catalog := range catalogList.Items { + channels, err := fetchCatalogMetadata[catalogmetadata.Channel](ctx, c.cl, catalog.Name, declcfg.SchemaChannel) + if err != nil { + return nil, err + } + + bundles, err := fetchCatalogMetadata[catalogmetadata.Bundle](ctx, c.cl, catalog.Name, declcfg.SchemaBundle) + if err != nil { + return nil, err + } + + bundles, err = populateExtraFields(catalog.Name, channels, bundles) + if err != nil { + return nil, err + } + + allBundles = append(allBundles, bundles...) + } + + return allBundles, nil +} + +func fetchCatalogMetadata[T catalogmetadata.Schemas](ctx context.Context, cl client.Client, catalogName, schema string) ([]*T, error) { + var cmList catalogd.CatalogMetadataList + err := cl.List(ctx, &cmList, client.MatchingLabels{"catalog": catalogName, "schema": schema}) + if err != nil { + return nil, err + } + + content, err := catalogmetadata.Unmarshal[T](cmList.Items) + if err != nil { + return nil, fmt.Errorf("error unmarshalling catalog metadata: %s", err) + } + + return content, nil +} + +func populateExtraFields(catalogName string, channels []*catalogmetadata.Channel, bundles []*catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { + bundlesMap := map[string]*catalogmetadata.Bundle{} + for i := range bundles { + bundleKey := fmt.Sprintf("%s-%s", bundles[i].Package, bundles[i].Name) + bundlesMap[bundleKey] = bundles[i] + + bundles[i].CatalogName = catalogName + } + + for _, ch := range channels { + for _, chEntry := range ch.Entries { + bundleKey := fmt.Sprintf("%s-%s", ch.Package, chEntry.Name) + bundle, ok := bundlesMap[bundleKey] + if !ok { + return nil, fmt.Errorf("bundle %q not found in catalog %q (package %q, channel %q)", chEntry.Name, catalogName, ch.Package, ch.Name) + } + + bundle.InChannels = append(bundle.InChannels, ch) + } + } + + return bundles, nil +} diff --git a/internal/catalogmetadata/client/client_test.go b/internal/catalogmetadata/client/client_test.go new file mode 100644 index 000000000..4b28dadd9 --- /dev/null +++ b/internal/catalogmetadata/client/client_test.go @@ -0,0 +1,321 @@ +package client_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + catalogClient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client" +) + +var ( + scheme *runtime.Scheme +) + +func init() { + scheme = runtime.NewScheme() + utilruntime.Must(catalogd.AddToScheme(scheme)) +} + +func TestClient(t *testing.T) { + t.Run("Bundles", func(t *testing.T) { + for _, tt := range []struct { + name string + fakeCatalog func() ([]client.Object, []*catalogmetadata.Bundle) + wantErr string + }{ + { + name: "valid catalog", + fakeCatalog: defaultFakeCatalog, + }, + { + name: "channel has a ref to a missing bundle", + fakeCatalog: func() ([]client.Object, []*catalogmetadata.Bundle) { + objs, _ := defaultFakeCatalog() + + objs = append(objs, &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1-channel-with-missing-bundle", + Labels: map[string]string{"schema": declcfg.SchemaChannel, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(`{ + "schema": "olm.channel", + "name": "channel-with-missing-bundle", + "package": "fake1", + "entries": [ + { + "name": "fake1.v9.9.9" + } + ] + }`), + }, + }) + + return objs, nil + }, + wantErr: `bundle "fake1.v9.9.9" not found in catalog "catalog-1" (package "fake1", channel "channel-with-missing-bundle")`, + }, + { + name: "invalid bundle", + fakeCatalog: func() ([]client.Object, []*catalogmetadata.Bundle) { + objs, _ := defaultFakeCatalog() + + objs = append(objs, &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-broken-bundle", + Labels: map[string]string{"schema": declcfg.SchemaBundle, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(`{"name":123123123}`), + }, + }) + + return objs, nil + }, + wantErr: "error unmarshalling catalog metadata: json: cannot unmarshal number into Go struct field Bundle.name of type string", + }, + { + name: "invalid channel", + fakeCatalog: func() ([]client.Object, []*catalogmetadata.Bundle) { + objs, _ := defaultFakeCatalog() + + objs = append(objs, &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1-broken-channel", + Labels: map[string]string{"schema": declcfg.SchemaChannel, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(`{"name":123123123}`), + }, + }) + + return objs, nil + }, + wantErr: "error unmarshalling catalog metadata: json: cannot unmarshal number into Go struct field Channel.name of type string", + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + objs, expectedBundles := tt.fakeCatalog() + + fakeCatalogClient := catalogClient.NewClient( + fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build(), + ) + + bundles, err := fakeCatalogClient.Bundles(ctx) + if tt.wantErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErr) + } + assert.Equal(t, expectedBundles, bundles) + }) + } + }) +} + +func defaultFakeCatalog() ([]client.Object, []*catalogmetadata.Bundle) { + package1 := `{ + "schema": "olm.bundle", + "name": "fake1" + }` + + bundle1 := `{ + "schema": "olm.bundle", + "name": "fake1.v1.0.0", + "package": "fake1", + "image": "fake-image", + "properties": [ + { + "type": "olm.package", + "value": {"packageName":"fake1","version":"1.0.0"} + } + ] + }` + + stableChannel := `{ + "schema": "olm.channel", + "name": "stable", + "package": "fake1", + "entries": [ + { + "name": "fake1.v1.0.0" + } + ] + }` + + betaChannel := `{ + "schema": "olm.channel", + "name": "beta", + "package": "fake1", + "entries": [ + { + "name": "fake1.v1.0.0" + } + ] + }` + + objs := []client.Object{ + &catalogd.Catalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1", + }, + }, + &catalogd.Catalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-2", + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1", + Labels: map[string]string{"schema": declcfg.SchemaPackage, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(package1), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1-channel-stable", + Labels: map[string]string{"schema": declcfg.SchemaChannel, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(stableChannel), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1-channel-beta", + Labels: map[string]string{"schema": declcfg.SchemaChannel, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(betaChannel), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-1-fake1-bundle-1", + Labels: map[string]string{"schema": declcfg.SchemaBundle, "catalog": "catalog-1"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(bundle1), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-2-fake1", + Labels: map[string]string{"schema": declcfg.SchemaPackage, "catalog": "catalog-2"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(package1), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-2-fake1-channel-stable", + Labels: map[string]string{"schema": declcfg.SchemaChannel, "catalog": "catalog-2"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(stableChannel), + }, + }, + &catalogd.CatalogMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog-2-fake1-bundle-1", + Labels: map[string]string{"schema": declcfg.SchemaBundle, "catalog": "catalog-2"}, + }, + Spec: catalogd.CatalogMetadataSpec{ + Content: json.RawMessage(bundle1), + }, + }, + } + + expectedBundles := []*catalogmetadata.Bundle{ + { + CatalogName: "catalog-1", + Bundle: declcfg.Bundle{ + Schema: declcfg.SchemaBundle, + Name: "fake1.v1.0.0", + Package: "fake1", + Image: "fake-image", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName":"fake1","version":"1.0.0"}`), + }, + }, + }, + InChannels: []*catalogmetadata.Channel{ + { + Channel: declcfg.Channel{ + Schema: declcfg.SchemaChannel, + Name: "beta", + Package: "fake1", + Entries: []declcfg.ChannelEntry{ + { + Name: "fake1.v1.0.0", + }, + }, + }, + }, + { + Channel: declcfg.Channel{ + Schema: declcfg.SchemaChannel, + Name: "stable", + Package: "fake1", + Entries: []declcfg.ChannelEntry{ + { + Name: "fake1.v1.0.0", + }, + }, + }, + }, + }, + }, + { + CatalogName: "catalog-2", + Bundle: declcfg.Bundle{ + Schema: declcfg.SchemaBundle, + Name: "fake1.v1.0.0", + Package: "fake1", + Image: "fake-image", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName":"fake1","version":"1.0.0"}`), + }, + }, + }, + InChannels: []*catalogmetadata.Channel{ + { + Channel: declcfg.Channel{ + Schema: declcfg.SchemaChannel, + Name: "stable", + Package: "fake1", + Entries: []declcfg.ChannelEntry{ + { + Name: "fake1.v1.0.0", + }, + }, + }, + }, + }, + }, + } + + return objs, expectedBundles +} diff --git a/internal/catalogmetadata/types.go b/internal/catalogmetadata/types.go index 52d55248c..624ab4eec 100644 --- a/internal/catalogmetadata/types.go +++ b/internal/catalogmetadata/types.go @@ -41,7 +41,8 @@ func (g GVKRequired) AsGVK() GVK { type Bundle struct { declcfg.Bundle - InChannels []*Channel + CatalogName string + InChannels []*Channel mu sync.RWMutex // these properties are lazy loaded as they are requested From d555222f4c7f38ff186fd62eb0fd675afc35cc5d Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Fri, 8 Sep 2023 16:57:33 +0100 Subject: [PATCH 5/5] Adds sort subpackage to catalogmetadata Signed-off-by: Mikalai Radchuk --- internal/catalogmetadata/sort/sort.go | 33 +++++++++ internal/catalogmetadata/sort/sort_test.go | 83 ++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 internal/catalogmetadata/sort/sort.go create mode 100644 internal/catalogmetadata/sort/sort_test.go diff --git a/internal/catalogmetadata/sort/sort.go b/internal/catalogmetadata/sort/sort.go new file mode 100644 index 000000000..7b220b7db --- /dev/null +++ b/internal/catalogmetadata/sort/sort.go @@ -0,0 +1,33 @@ +package sort + +import ( + "github.com/operator-framework/operator-controller/internal/catalogmetadata" +) + +// ByVersion is a sort "less" function that orders bundles +// in inverse version order (higher versions on top). +func ByVersion(b1, b2 *catalogmetadata.Bundle) bool { + ver1, err1 := b1.Version() + ver2, err2 := b2.Version() + if err1 != nil || err2 != nil { + return compareErrors(err1, err2) < 0 + } + + // Check for "greater than" because + // we want higher versions on top + return ver1.GT(*ver2) +} + +// compareErrors returns 0 if both errors are either nil or not nil +// -1 if err1 is nil and err2 is not nil +// +1 if err1 is not nil and err2 is nil +func compareErrors(err1 error, err2 error) int { + if err1 != nil && err2 == nil { + return 1 + } + + if err1 == nil && err2 != nil { + return -1 + } + return 0 +} diff --git a/internal/catalogmetadata/sort/sort_test.go b/internal/catalogmetadata/sort/sort_test.go new file mode 100644 index 000000000..b18ce616f --- /dev/null +++ b/internal/catalogmetadata/sort/sort_test.go @@ -0,0 +1,83 @@ +package sort_test + +import ( + "encoding/json" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" +) + +func TestByVersion(t *testing.T) { + b1 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "package1.v1.0.0", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0"}`), + }, + }, + }} + b2 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "package1.v0.0.1", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "0.0.1"}`), + }, + }, + }} + b3 := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "package1.v1.0.0-alpha+001", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0-alpha+001"}`), + }, + }, + }} + b4noVersion := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "package1.no-version", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1"}`), + }, + }, + }} + b5empty := &catalogmetadata.Bundle{Bundle: declcfg.Bundle{ + Name: "package1.empty", + }} + + t.Run("all bundles valid", func(t *testing.T) { + toSort := []*catalogmetadata.Bundle{b3, b2, b1} + sort.SliceStable(toSort, func(i, j int) bool { + return catalogsort.ByVersion(toSort[i], toSort[j]) + }) + + assert.Len(t, toSort, 3) + assert.Equal(t, b1, toSort[0]) + assert.Equal(t, b3, toSort[1]) + assert.Equal(t, b2, toSort[2]) + }) + + t.Run("some bundles are missing version", func(t *testing.T) { + toSort := []*catalogmetadata.Bundle{b3, b4noVersion, b2, b5empty, b1} + sort.SliceStable(toSort, func(i, j int) bool { + return catalogsort.ByVersion(toSort[i], toSort[j]) + }) + + assert.Len(t, toSort, 5) + assert.Equal(t, b1, toSort[0]) + assert.Equal(t, b3, toSort[1]) + assert.Equal(t, b2, toSort[2]) + assert.Equal(t, b4noVersion, toSort[3]) + assert.Equal(t, b5empty, toSort[4]) + }) +}