From 3cd59a26059f749337f4f651ef99a370f57a5296 Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Mon, 4 Sep 2023 17:10:02 +0100 Subject: [PATCH] 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 | 47 +++++++++ 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, 276 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..ff8f46fb5 --- /dev/null +++ b/internal/catalogmetadata/filter.go @@ -0,0 +1,47 @@ +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 { + eval := true + for _, predicate := range predicates { + eval = eval && predicate(obj) + if !eval { + return false + } + } + return eval + } +} + +func Or[T Schemas](predicates ...Predicate[T]) Predicate[T] { + return func(obj *T) bool { + eval := false + for _, predicate := range predicates { + eval = eval || predicate(obj) + if eval { + return true + } + } + return eval + } +} + +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..3df1612b4 --- /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{ + {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{ + {declcfg.Bundle{Name: "operator1.v2", Package: "operator1", Image: "fake2"}}, + {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{ + {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{ + {declcfg.Bundle{Name: "operator1.v1", Package: "operator1", Image: "fake1"}}, + {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) + } + }) + } +}