From c36f27350fba61a0930f2dd009cb97477b7e6d88 Mon Sep 17 00:00:00 2001 From: Varsha Prasad Narsing Date: Tue, 2 May 2023 12:08:15 -0400 Subject: [PATCH] [revise] Remove Entity and EntitySource Removes Entity and EntitySource. The solver takes in the Variable Source directly. The constraints, filters, sort or any transformation should be performed on variables before providing it to solver. Signed-off-by: Varsha Prasad Narsing --- cmd/dimacs/cmd.go | 2 +- cmd/dimacs/dimacs_constraints.go | 11 +- cmd/dimacs/dimacs_source.go | 24 -- cmd/sudoku/cmd.go | 4 +- cmd/sudoku/sudoku.go | 26 +- pkg/deppy/input/cache_entity_source.go | 58 --- pkg/deppy/input/entity.go | 21 - pkg/deppy/input/entity_source.go | 31 -- pkg/deppy/input/entity_source_test.go | 205 ---------- pkg/deppy/input/entity_test.go | 28 -- pkg/deppy/input/query.go | 62 --- pkg/deppy/input/variable_source.go | 2 +- pkg/deppy/solver/solver.go | 7 +- pkg/deppy/solver/solver_test.go | 65 ++-- pkg/ext/olm/constraints.go | 276 +------------ pkg/ext/olm/constraints_test.go | 518 ------------------------- 16 files changed, 50 insertions(+), 1290 deletions(-) delete mode 100644 cmd/dimacs/dimacs_source.go delete mode 100644 pkg/deppy/input/cache_entity_source.go delete mode 100644 pkg/deppy/input/entity.go delete mode 100644 pkg/deppy/input/entity_source.go delete mode 100644 pkg/deppy/input/entity_source_test.go delete mode 100644 pkg/deppy/input/entity_test.go delete mode 100644 pkg/deppy/input/query.go delete mode 100644 pkg/ext/olm/constraints_test.go diff --git a/cmd/dimacs/cmd.go b/cmd/dimacs/cmd.go index 324a5a8..3239831 100644 --- a/cmd/dimacs/cmd.go +++ b/cmd/dimacs/cmd.go @@ -53,7 +53,7 @@ func solve(path string) error { } // build solver - so, err := solver.NewDeppySolver(NewDimacsEntitySource(dimacs), NewDimacsVariableSource(dimacs)) + so, err := solver.NewDeppySolver(NewDimacsVariableSource(dimacs)) if err != nil { return err } diff --git a/cmd/dimacs/dimacs_constraints.go b/cmd/dimacs/dimacs_constraints.go index 3b87989..86cbc82 100644 --- a/cmd/dimacs/dimacs_constraints.go +++ b/cmd/dimacs/dimacs_constraints.go @@ -21,16 +21,13 @@ func NewDimacsVariableSource(dimacs *Dimacs) *ConstraintGenerator { } } -func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { +func (d *ConstraintGenerator) GetVariables(ctx context.Context) ([]deppy.Variable, error) { varMap := make(map[deppy.Identifier]*input.SimpleVariable, len(d.dimacs.variables)) variables := make([]deppy.Variable, 0, len(d.dimacs.variables)) - if err := entitySource.Iterate(ctx, func(entity *input.Entity) error { - variable := input.NewSimpleVariable(entity.Identifier()) + + for _, id := range d.dimacs.variables { + variable := input.NewSimpleVariable(deppy.IdentifierFromString(id)) variables = append(variables, variable) - varMap[entity.Identifier()] = variable - return nil - }); err != nil { - return nil, err } // create constraints out of the clauses diff --git a/cmd/dimacs/dimacs_source.go b/cmd/dimacs/dimacs_source.go deleted file mode 100644 index 0808528..0000000 --- a/cmd/dimacs/dimacs_source.go +++ /dev/null @@ -1,24 +0,0 @@ -package dimacs - -import ( - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" -) - -var _ input.EntitySource = &EntitySource{} - -type EntitySource struct { - *input.CacheEntitySource -} - -func NewDimacsEntitySource(dimacs *Dimacs) *EntitySource { - entities := make(map[deppy.Identifier]input.Entity, len(dimacs.Variables())) - for _, variable := range dimacs.Variables() { - id := deppy.Identifier(variable) - entities[id] = *input.NewEntity(id, nil) - } - - return &EntitySource{ - CacheEntitySource: input.NewCacheQuerier(entities), - } -} diff --git a/cmd/sudoku/cmd.go b/cmd/sudoku/cmd.go index bf068a4..891746a 100644 --- a/cmd/sudoku/cmd.go +++ b/cmd/sudoku/cmd.go @@ -23,8 +23,8 @@ func NewSudokuCommand() *cobra.Command { func solve() error { // build solver - sudoku := NewSudoku() - so, err := solver.NewDeppySolver(sudoku, sudoku) + sudoku := &Sudoku{} + so, err := solver.NewDeppySolver(sudoku) if err != nil { return err } diff --git a/cmd/sudoku/sudoku.go b/cmd/sudoku/sudoku.go index f68897e..32e238d 100644 --- a/cmd/sudoku/sudoku.go +++ b/cmd/sudoku/sudoku.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math/rand" - "strconv" "time" "github.com/operator-framework/deppy/pkg/deppy" @@ -12,11 +11,11 @@ import ( "github.com/operator-framework/deppy/pkg/deppy/input" ) -var _ input.EntitySource = &Sudoku{} var _ input.VariableSource = &Sudoku{} +// TODO: this could be an empty struct type Sudoku struct { - *input.CacheEntitySource + variables []deppy.Variable } func GetID(row int, col int, num int) deppy.Identifier { @@ -26,26 +25,7 @@ func GetID(row int, col int, num int) deppy.Identifier { return deppy.Identifier(fmt.Sprintf("%03d", n)) } -func NewSudoku() *Sudoku { - var entities = make(map[deppy.Identifier]input.Entity, 9*9*9) - for row := 0; row < 9; row++ { - for col := 0; col < 9; col++ { - for num := 0; num < 9; num++ { - id := GetID(row, col, num) - entities[id] = *input.NewEntity(id, map[string]string{ - "row": strconv.Itoa(row), - "col": strconv.Itoa(col), - "num": strconv.Itoa(num), - }) - } - } - } - return &Sudoku{ - CacheEntitySource: input.NewCacheQuerier(entities), - } -} - -func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]deppy.Variable, error) { +func (s Sudoku) GetVariables(ctx context.Context) ([]deppy.Variable, error) { // adapted from: https://github.com/go-air/gini/blob/871d828a26852598db2b88f436549634ba9533ff/sudoku_test.go#L10 variables := make(map[deppy.Identifier]*input.SimpleVariable, 0) inorder := make([]deppy.Variable, 0) diff --git a/pkg/deppy/input/cache_entity_source.go b/pkg/deppy/input/cache_entity_source.go deleted file mode 100644 index 8078118..0000000 --- a/pkg/deppy/input/cache_entity_source.go +++ /dev/null @@ -1,58 +0,0 @@ -package input - -import ( - "context" - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" -) - -var _ EntitySource = &CacheEntitySource{} - -type CacheEntitySource struct { - // TODO: separate out a cache - entities map[deppy.Identifier]Entity -} - -func NewCacheQuerier(entities map[deppy.Identifier]Entity) *CacheEntitySource { - return &CacheEntitySource{ - entities: entities, - } -} - -func (c CacheEntitySource) Get(_ context.Context, id deppy.Identifier) (*Entity, error) { - if entity, ok := c.entities[id]; ok { - return &entity, nil - } - return nil, fmt.Errorf("entity with id: %s not found in the entity source", id.String()) -} - -func (c CacheEntitySource) Filter(_ context.Context, filter Predicate) (EntityList, error) { - resultSet := EntityList{} - for _, entity := range c.entities { - if filter(&entity) { - resultSet = append(resultSet, entity) - } - } - return resultSet, nil -} - -func (c CacheEntitySource) GroupBy(_ context.Context, fn GroupByFunction) (EntityListMap, error) { - resultSet := EntityListMap{} - for _, entity := range c.entities { - keys := fn(&entity) - for _, key := range keys { - resultSet[key] = append(resultSet[key], entity) - } - } - return resultSet, nil -} - -func (c CacheEntitySource) Iterate(_ context.Context, fn IteratorFunction) error { - for _, entity := range c.entities { - if err := fn(&entity); err != nil { - return err - } - } - return nil -} diff --git a/pkg/deppy/input/entity.go b/pkg/deppy/input/entity.go deleted file mode 100644 index 6bcd0b3..0000000 --- a/pkg/deppy/input/entity.go +++ /dev/null @@ -1,21 +0,0 @@ -package input - -import ( - "github.com/operator-framework/deppy/pkg/deppy" -) - -type Entity struct { - ID deppy.Identifier `json:"identifier"` - Properties map[string]string `json:"properties"` -} - -func (e *Entity) Identifier() deppy.Identifier { - return e.ID -} - -func NewEntity(id deppy.Identifier, properties map[string]string) *Entity { - return &Entity{ - ID: id, - Properties: properties, - } -} diff --git a/pkg/deppy/input/entity_source.go b/pkg/deppy/input/entity_source.go deleted file mode 100644 index 4cd7b01..0000000 --- a/pkg/deppy/input/entity_source.go +++ /dev/null @@ -1,31 +0,0 @@ -package input - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/deppy" -) - -// IteratorFunction is executed for each entity when iterating over all entities -type IteratorFunction func(entity *Entity) error - -// SortFunction returns true if e1 is less than e2 -type SortFunction func(e1 *Entity, e2 *Entity) bool - -// GroupByFunction transforms an entity into a slice of keys (strings) -// over which the entities will be grouped by -type GroupByFunction func(e1 *Entity) []string - -// Predicate returns true if the entity should be kept when filtering -type Predicate func(entity *Entity) bool - -type EntityList []Entity -type EntityListMap map[string]EntityList - -// EntitySource provides a query and content acquisition interface for arbitrary entity stores -type EntitySource interface { - Get(ctx context.Context, id deppy.Identifier) (*Entity, error) - Filter(ctx context.Context, filter Predicate) (EntityList, error) - GroupBy(ctx context.Context, fn GroupByFunction) (EntityListMap, error) - Iterate(ctx context.Context, fn IteratorFunction) error -} diff --git a/pkg/deppy/input/entity_source_test.go b/pkg/deppy/input/entity_source_test.go deleted file mode 100644 index 79c8ba8..0000000 --- a/pkg/deppy/input/entity_source_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package input_test - -import ( - "context" - "fmt" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/deppy/pkg/deppy" - - . "github.com/onsi/gomega/gstruct" -) - -func TestInput(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Input Suite") -} - -// Test functions for filter -func byIndex(index string) input.Predicate { - return func(entity *input.Entity) bool { - i, ok := entity.Properties["index"] - if !ok { - return false - } - if i == index { - return true - } - return false - } -} -func bySource(source string) input.Predicate { - return func(entity *input.Entity) bool { - i, ok := entity.Properties["source"] - if !ok { - return false - } - if i == source { - return true - } - return false - } -} - -// Test function for iterate -var entityCheck map[deppy.Identifier]bool - -func check(entity *input.Entity) error { - checked, ok := entityCheck[entity.Identifier()] - Expect(ok).Should(BeTrue()) - Expect(checked).Should(BeFalse()) - entityCheck[entity.Identifier()] = true - return nil -} - -// Test function for GroupBy -func bySourceAndIndex(entity *input.Entity) []string { - switch entity.Identifier() { - case "1-1": - return []string{"source 1", "index 1"} - case "1-2": - return []string{"source 1", "index 2"} - case "2-1": - return []string{"source 2", "index 1"} - case "2-2": - return []string{"source 2", "index 2"} - } - return nil -} - -var _ = Describe("EntitySource", func() { - When("a group is created with multiple entity sources", func() { - var ( - entitySource input.EntitySource - ) - - BeforeEach(func() { - entities := map[deppy.Identifier]input.Entity{ - deppy.Identifier("1-1"): *input.NewEntity("1-1", map[string]string{"source": "1", "index": "1"}), - deppy.Identifier("1-2"): *input.NewEntity("1-2", map[string]string{"source": "1", "index": "2"}), - deppy.Identifier("2-1"): *input.NewEntity("2-1", map[string]string{"source": "2", "index": "1"}), - deppy.Identifier("2-2"): *input.NewEntity("2-2", map[string]string{"source": "2", "index": "2"}), - } - entitySource = input.NewCacheQuerier(entities) - }) - - Describe("Get", func() { - It("should return requested entity", func() { - e, err := entitySource.Get(context.Background(), "2-2") - Expect(err).To(BeNil()) - Expect(e).NotTo(BeNil()) - Expect(e.Identifier()).To(Equal(deppy.Identifier("2-2"))) - }) - - It("should return an error when the requested entity is not found", func() { - e, err := entitySource.Get(context.Background(), "random") - Expect(err).To(HaveOccurred()) - Expect(e).To(BeNil()) - Expect(err.Error()).To(BeEquivalentTo(fmt.Sprintf("entity with id: %s not found in the entity source", "random"))) - }) - }) - - Describe("Filter", func() { - It("should return entities that meet filter predicates", func() { - id := func(element interface{}) string { - return fmt.Sprintf("%v", element) - } - el, err := entitySource.Filter(context.Background(), input.Or(byIndex("2"), bySource("1"))) - Expect(err).To(BeNil()) - Expect(el).To(MatchAllElements(id, Elements{ - "{1-2 map[index:2 source:1]}": Not(BeNil()), - "{2-2 map[index:2 source:2]}": Not(BeNil()), - "{1-1 map[index:1 source:1]}": Not(BeNil()), - })) - ids := el.CollectIds() - Expect(ids).NotTo(BeNil()) - Expect(ids).To(MatchAllElements(id, Elements{ - "1-2": Not(BeNil()), - "2-2": Not(BeNil()), - "1-1": Not(BeNil()), - })) - - el, err = entitySource.Filter(context.Background(), input.And(byIndex("2"), bySource("1"))) - Expect(err).To(BeNil()) - Expect(el).To(MatchAllElements(id, Elements{ - "{1-2 map[index:2 source:1]}": Not(BeNil()), - })) - ids = el.CollectIds() - Expect(ids).NotTo(BeNil()) - Expect(ids).To(MatchAllElements(id, Elements{ - "1-2": Not(BeNil()), - })) - - el, err = entitySource.Filter(context.Background(), input.And(byIndex("2"), input.Not(bySource("1")))) - Expect(err).To(BeNil()) - Expect(el).To(MatchAllElements(id, Elements{ - "{2-2 map[index:2 source:2]}": Not(BeNil()), - })) - ids = el.CollectIds() - Expect(ids).NotTo(BeNil()) - Expect(ids).To(MatchAllElements(id, Elements{ - "2-2": Not(BeNil()), - })) - - }) - }) - - Describe("Iterate", func() { - It("should go through all entities", func() { - entityCheck = map[deppy.Identifier]bool{"1-1": false, "1-2": false, "2-1": false, "2-2": false} - err := entitySource.Iterate(context.Background(), check) - Expect(err).To(BeNil()) - for _, value := range entityCheck { - Expect(value).To(BeTrue()) - } - }) - }) - - Describe("GroupBy", func() { - It("should group entities by the keys provided by the groupBy function", func() { - id := func(element interface{}) string { - return fmt.Sprintf("%v", element) - } - grouped, err := entitySource.GroupBy(context.Background(), bySourceAndIndex) - Expect(err).To(BeNil()) - Expect(grouped).To(MatchAllKeys(Keys{ - "index 1": Not(BeNil()), - "index 2": Not(BeNil()), - "source 1": Not(BeNil()), - "source 2": Not(BeNil()), - })) - for key, value := range grouped { - switch key { - case "index 1": - Expect(value).To(MatchAllElements(id, Elements{ - "{1-1 map[index:1 source:1]}": Not(BeNil()), - "{2-1 map[index:1 source:2]}": Not(BeNil()), - })) - case "index 2": - Expect(value).To(MatchAllElements(id, Elements{ - "{1-2 map[index:2 source:1]}": Not(BeNil()), - "{2-2 map[index:2 source:2]}": Not(BeNil()), - })) - case "source 1": - Expect(value).To(MatchAllElements(id, Elements{ - "{1-1 map[index:1 source:1]}": Not(BeNil()), - "{1-2 map[index:2 source:1]}": Not(BeNil()), - })) - case "source 2": - Expect(value).To(MatchAllElements(id, Elements{ - "{2-1 map[index:1 source:2]}": Not(BeNil()), - "{2-2 map[index:2 source:2]}": Not(BeNil()), - })) - default: - Fail(fmt.Sprintf("unknown key %s", key)) - } - } - }) - }) - }) -}) diff --git a/pkg/deppy/input/entity_test.go b/pkg/deppy/input/entity_test.go deleted file mode 100644 index c926d6d..0000000 --- a/pkg/deppy/input/entity_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package input_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/deppy/pkg/deppy" -) - -var _ = Describe("Entity", func() { - It("stores id and properties", func() { - entity := input.NewEntity("id", map[string]string{"prop": "value"}) - Expect(entity.Identifier()).To(Equal(deppy.Identifier("id"))) - - value, ok := entity.Properties["prop"] - Expect(ok).To(BeTrue()) - Expect(value).To(Equal("value")) - }) - - It("returns not found error when property is not found", func() { - entity := input.NewEntity("id", map[string]string{"foo": "value"}) - value, ok := entity.Properties["bar"] - Expect(value).To(Equal("")) - Expect(ok).To(BeFalse()) - }) -}) diff --git a/pkg/deppy/input/query.go b/pkg/deppy/input/query.go deleted file mode 100644 index 4838545..0000000 --- a/pkg/deppy/input/query.go +++ /dev/null @@ -1,62 +0,0 @@ -package input - -import ( - "sort" - - "github.com/operator-framework/deppy/pkg/deppy" -) - -func (r EntityList) Sort(fn SortFunction) EntityList { - sort.SliceStable(r, func(i, j int) bool { - return fn(&r[i], &r[j]) - }) - return r -} - -func (r EntityList) CollectIds() []deppy.Identifier { - ids := make([]deppy.Identifier, len(r)) - for i := range r { - ids[i] = r[i].Identifier() - } - return ids -} - -func (g EntityListMap) Sort(fn SortFunction) EntityListMap { - for key := range g { - sort.SliceStable(g[key], func(i, j int) bool { - return fn(&g[key][i], &g[key][j]) - }) - } - return g -} -func And(predicates ...Predicate) Predicate { - return func(entity *Entity) bool { - eval := true - for _, predicate := range predicates { - eval = eval && predicate(entity) - if !eval { - return false - } - } - return eval - } -} - -func Or(predicates ...Predicate) Predicate { - return func(entity *Entity) bool { - eval := false - for _, predicate := range predicates { - eval = eval || predicate(entity) - if eval { - return true - } - } - return eval - } -} - -func Not(predicate Predicate) Predicate { - return func(entity *Entity) bool { - return !predicate(entity) - } -} diff --git a/pkg/deppy/input/variable_source.go b/pkg/deppy/input/variable_source.go index 2490831..41514c5 100644 --- a/pkg/deppy/input/variable_source.go +++ b/pkg/deppy/input/variable_source.go @@ -8,7 +8,7 @@ import ( // VariableSource generates solver constraints given an entity querier interface type VariableSource interface { - GetVariables(ctx context.Context, entitySource EntitySource) ([]deppy.Variable, error) + GetVariables(ctx context.Context) ([]deppy.Variable, error) } var _ deppy.Variable = &SimpleVariable{} diff --git a/pkg/deppy/solver/solver.go b/pkg/deppy/solver/solver.go index e346dfd..0012c1e 100644 --- a/pkg/deppy/solver/solver.go +++ b/pkg/deppy/solver/solver.go @@ -77,13 +77,12 @@ func AddAllVariablesToSolution() Option { // DeppySolver is a simple solver implementation that takes an entity source group and a constraint aggregator // to produce a Solution (or error if no solution can be found) type DeppySolver struct { - entitySource input.EntitySource variableSource input.VariableSource } -func NewDeppySolver(entitySource input.EntitySource, variableSource input.VariableSource) (*DeppySolver, error) { +// TODO: Make solver to accept multiple variable sources +func NewDeppySolver(variableSource input.VariableSource) (*DeppySolver, error) { return &DeppySolver{ - entitySource: entitySource, variableSource: variableSource, }, nil } @@ -91,7 +90,7 @@ func NewDeppySolver(entitySource input.EntitySource, variableSource input.Variab func (d DeppySolver) Solve(ctx context.Context, options ...Option) (*Solution, error) { solutionOpts := defaultSolutionOptions().apply(options...) - vars, err := d.variableSource.GetVariables(ctx, d.entitySource) + vars, err := d.variableSource.GetVariables(ctx) if err != nil { return nil, err } diff --git a/pkg/deppy/solver/solver_test.go b/pkg/deppy/solver/solver_test.go index fcfec03..7bb932c 100644 --- a/pkg/deppy/solver/solver_test.go +++ b/pkg/deppy/solver/solver_test.go @@ -23,35 +23,23 @@ func TestSolver(t *testing.T) { RunSpecs(t, "Solver Suite") } -type EntitySourceStruct struct { +type VariableSourceStruct struct { variables []deppy.Variable - input.EntitySource } -func (c EntitySourceStruct) GetVariables(_ context.Context, _ input.EntitySource) ([]deppy.Variable, error) { +func (c VariableSourceStruct) GetVariables(_ context.Context) ([]deppy.Variable, error) { return c.variables, nil } -func NewEntitySource(variables []deppy.Variable) *EntitySourceStruct { - entities := make(map[deppy.Identifier]input.Entity, len(variables)) - for _, variable := range variables { - entityID := variable.Identifier() - entities[entityID] = *input.NewEntity(entityID, map[string]string{"x": "y"}) - } - return &EntitySourceStruct{ - variables: variables, - EntitySource: input.NewCacheQuerier(entities), - } -} - var _ = Describe("Entity", func() { It("should select a mandatory entity", func() { variables := []deppy.Variable{ input.NewSimpleVariable("1", constraint.Mandatory()), input.NewSimpleVariable("2"), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -66,8 +54,9 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("1", constraint.Mandatory()), input.NewSimpleVariable("2", constraint.Mandatory()), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -83,9 +72,8 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("2"), input.NewSimpleVariable("3"), } - s := NewEntitySource(variables) - - so, err := solver.NewDeppySolver(s, s) + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -101,8 +89,9 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("2", constraint.Prohibited()), input.NewSimpleVariable("3"), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -110,8 +99,7 @@ var _ = Describe("Entity", func() { }) It("should return peripheral errors", func() { - s := NewEntitySource(nil) - so, err := solver.NewDeppySolver(s, FailingVariableSource{}) + so, err := solver.NewDeppySolver(FailingVariableSource{}) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).To(HaveOccurred()) @@ -124,8 +112,9 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("2"), input.NewSimpleVariable("3", constraint.Prohibited()), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -141,8 +130,9 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("2"), input.NewSimpleVariable("3", constraint.Prohibited()), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background(), solver.AddAllVariablesToSolution()) Expect(err).ToNot(HaveOccurred()) @@ -160,8 +150,9 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("3", constraint.Prohibited()), input.NewSimpleVariable("4"), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -178,8 +169,8 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("3", constraint.AtMost(1, "3", "4")), input.NewSimpleVariable("4"), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -198,8 +189,8 @@ var _ = Describe("Entity", func() { input.NewSimpleVariable("5"), input.NewSimpleVariable("6"), } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) + varSrcStruct := &VariableSourceStruct{variables: variables} + so, err := solver.NewDeppySolver(varSrcStruct) Expect(err).ToNot(HaveOccurred()) solution, err := so.Solve(context.Background()) Expect(err).ToNot(HaveOccurred()) @@ -217,6 +208,6 @@ var _ input.VariableSource = &FailingVariableSource{} type FailingVariableSource struct { } -func (f FailingVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { +func (f FailingVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { return nil, fmt.Errorf("error") } diff --git a/pkg/ext/olm/constraints.go b/pkg/ext/olm/constraints.go index adaae3e..8e7b699 100644 --- a/pkg/ext/olm/constraints.go +++ b/pkg/ext/olm/constraints.go @@ -2,20 +2,19 @@ package olm import ( "context" - "fmt" - "regexp" "strings" - "github.com/blang/semver/v4" - "github.com/tidwall/gjson" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy" ) +// Question: +// These are not needed since while constructing the variables, its upto the users +// to sort, group by etc while providing it to the solver. Is this understanding right? +// Which means we will do the filtering based on constraints before constructing variables. +// Maybe we should create helpers for these separately keeping "bundle" as the domain. + const ( PropertyOLMGVK = "olm.gvk" PropertyOLMPackageName = "olm.packageName" @@ -34,19 +33,8 @@ type requirePackage struct { channel string } -func (r *requirePackage) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - resultSet, err := entitySource.Filter(ctx, input.And( - withPackageName(r.packageName), - withinVersion(r.versionRange), - withChannel(r.channel))) - if err != nil || len(resultSet) == 0 { - return nil, err - } - ids := resultSet.Sort(byChannelAndVersion).CollectIds() - subject := subject("require", r.packageName, r.versionRange, r.channel) - return []deppy.Variable{ - input.NewSimpleVariable(subject, constraint.Mandatory(), constraint.Dependency(ids...)), - }, nil +func (r *requirePackage) GetVariables(ctx context.Context) ([]deppy.Variable, error) { + return nil, nil } // RequirePackage creates a constraint generator to describe that a package is wanted for installation @@ -58,254 +46,6 @@ func RequirePackage(packageName string, versionRange string, channel string) inp } } -var _ input.VariableSource = &uniqueness{} - -type subjectFormatFn func(key string) deppy.Identifier - -type uniqueness struct { - subject subjectFormatFn - groupByFn input.GroupByFunction -} - -func (u *uniqueness) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - resultSet, err := entitySource.GroupBy(ctx, u.groupByFn) - if err != nil || len(resultSet) == 0 { - return nil, err - } - variables := make([]deppy.Variable, 0, len(resultSet)) - for key, entities := range resultSet { - ids := entities.Sort(byChannelAndVersion).CollectIds() - variables = append(variables, input.NewSimpleVariable(u.subject(key), constraint.AtMost(1, ids...))) - } - return variables, nil -} - -// GVKUniqueness generates constraints describing that only a single bundle / gvk can be selected -func GVKUniqueness() input.VariableSource { - return &uniqueness{ - subject: uniquenessSubjectFormat, - groupByFn: gvkGroupFunction, - } -} - -// PackageUniqueness generates constraints describing that only a single bundle / package can be selected -func PackageUniqueness() input.VariableSource { - return &uniqueness{ - subject: uniquenessSubjectFormat, - groupByFn: packageGroupFunction, - } -} - -func uniquenessSubjectFormat(key string) deppy.Identifier { - return deppy.IdentifierFromString(fmt.Sprintf("%s uniqueness", key)) -} - -var _ input.VariableSource = &packageDependency{} - -type packageDependency struct { - subject deppy.Identifier - packageName string - versionRange string -} - -func (p *packageDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - entities, err := entitySource.Filter(ctx, input.And(withPackageName(p.packageName), withinVersion(p.versionRange))) - if err != nil || len(entities) == 0 { - return nil, err - } - ids := entities.Sort(byChannelAndVersion).CollectIds() - return []deppy.Variable{input.NewSimpleVariable(p.subject, constraint.Dependency(ids...))}, nil -} - -// PackageDependency generates constraints to describe a package's dependency on another package -func PackageDependency(subject deppy.Identifier, packageName string, versionRange string) input.VariableSource { - return &packageDependency{ - subject: subject, - packageName: packageName, - versionRange: versionRange, - } -} - -var _ input.VariableSource = &gvkDependency{} - -type gvkDependency struct { - subject deppy.Identifier - group string - version string - kind string -} - -func (g *gvkDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - entities, err := entitySource.Filter(ctx, input.And(withExportsGVK(g.group, g.version, g.kind))) - if err != nil || len(entities) == 0 { - return nil, err - } - ids := entities.Sort(byChannelAndVersion).CollectIds() - return []deppy.Variable{input.NewSimpleVariable(g.subject, constraint.Dependency(ids...))}, nil -} - -// GVKDependency generates constraints to describe a package's dependency on a gvk -func GVKDependency(subject deppy.Identifier, group string, version string, kind string) input.VariableSource { - return &gvkDependency{ - subject: subject, - group: group, - version: version, - kind: kind, - } -} - -func withPackageName(packageName string) input.Predicate { - return func(entity *input.Entity) bool { - if pkgName, ok := entity.Properties[PropertyOLMPackageName]; ok { - return pkgName == packageName - } - return false - } -} - -func withinVersion(semverRange string) input.Predicate { - return func(entity *input.Entity) bool { - if v, ok := entity.Properties[PropertyOLMVersion]; ok { - vrange, err := semver.ParseRange(semverRange) - if err != nil { - return false - } - version, err := semver.Parse(v) - if err != nil { - return false - } - return vrange(version) - } - return false - } -} - -func withChannel(channel string) input.Predicate { - return func(entity *input.Entity) bool { - if channel == "" { - return true - } - if c, ok := entity.Properties[PropertyOLMChannel]; ok { - return c == channel - } - return false - } -} - -func withExportsGVK(group string, version string, kind string) input.Predicate { - return func(entity *input.Entity) bool { - if g, ok := entity.Properties[PropertyOLMGVK]; ok { - for _, gvk := range gjson.Parse(g).Array() { - if gjson.Get(gvk.String(), "group").String() == group && gjson.Get(gvk.String(), "version").String() == version && gjson.Get(gvk.String(), "kind").String() == kind { - return true - } - } - } - return false - } -} - -// byChannelAndVersion is an entity sort function that orders the entities in -// 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(e1 *input.Entity, e2 *input.Entity) bool { - idOrder := e1.Identifier() < e2.Identifier() - - // first sort package lexical order - pkgOrder := compareProperty(getPropertyOrNotFound(e1, PropertyOLMPackageName), getPropertyOrNotFound(e2, PropertyOLMPackageName)) - if pkgOrder != 0 { - return pkgOrder < 0 - } - - // then sort by channel order with default channel at the start and all other channels in lexical order - e1DefaultChannel := getPropertyOrNotFound(e1, PropertyOLMDefaultChannel) - e2DefaultChannel := getPropertyOrNotFound(e2, PropertyOLMDefaultChannel) - - e1Channel := getPropertyOrNotFound(e1, PropertyOLMChannel) - e2Channel := getPropertyOrNotFound(e2, PropertyOLMChannel) - channelOrder := compareProperty(e1Channel, e2Channel) - - // if both entities are from different channels - if channelOrder != 0 { - e1InDefaultChannel := e1Channel == e1DefaultChannel && e1Channel != propertyNotFound - e2InDefaultChannel := e2Channel == e2DefaultChannel && e2Channel != propertyNotFound - - // if one of them is in the default channel, promote it - // if both of them are in their default channels, stay with lexical channel order - if e1InDefaultChannel || e2InDefaultChannel && !(e1InDefaultChannel && e2InDefaultChannel) { - return e1InDefaultChannel - } - - // otherwise sort by channel lexical order - return channelOrder < 0 - } - - // if package and channel are the same, compare by version - e1Version := getPropertyOrNotFound(e1, PropertyOLMVersion) - e2Version := getPropertyOrNotFound(e2, PropertyOLMVersion) - - // if neither has a version property, sort in Identifier order - if e1Version == propertyNotFound && e2Version == propertyNotFound { - return idOrder - } - - // if one of the version is not found, not found is higher than found - if e1Version == propertyNotFound || e2Version == propertyNotFound { - return e1Version > e2Version - } - - // if one or both of the versions cannot be parsed, return id order - v1, err := semver.Parse(e1Version) - if err != nil { - return idOrder - } - v2, err := semver.Parse(e2Version) - if err != nil { - return idOrder - } - - // finally, order version from highest to lowest (favor the latest release) - return v1.GT(v2) -} - -func gvkGroupFunction(entity *input.Entity) []string { - if gvks, ok := entity.Properties[PropertyOLMGVK]; ok { - gvkArray := gjson.Parse(gvks).Array() - keys := make([]string, 0, len(gvkArray)) - for _, val := range gvkArray { - var group = val.Get("group") - var version = val.Get("version") - var kind = val.Get("kind") - if group.String() != "" && version.String() != "" && kind.String() != "" { - gvk := fmt.Sprintf("%s/%s/%s", group, version, kind) - keys = append(keys, gvk) - } - } - return keys - } - return nil -} - -func packageGroupFunction(entity *input.Entity) []string { - if packageName, ok := entity.Properties[PropertyOLMPackageName]; ok { - return []string{packageName} - } - return nil -} - -func subject(str ...string) deppy.Identifier { - return deppy.Identifier(regexp.MustCompile(`\\s`).ReplaceAllString(strings.Join(str, "-"), "")) -} - -func getPropertyOrNotFound(entity *input.Entity, propertyName string) string { - value, ok := entity.Properties[propertyName] - if !ok { - return propertyNotFound - } - return value -} - // compareProperty compares two entity property values. It works as strings.Compare // except when one of the properties is not found (""). It computes not found properties as higher than found. // If p1 is not found, it returns 1, if p2 is not found it returns -1 diff --git a/pkg/ext/olm/constraints_test.go b/pkg/ext/olm/constraints_test.go deleted file mode 100644 index db49b41..0000000 --- a/pkg/ext/olm/constraints_test.go +++ /dev/null @@ -1,518 +0,0 @@ -package olm_test - -import ( - "context" - "errors" - "fmt" - "sort" - "strings" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - . "github.com/onsi/gomega/gstruct" - - "github.com/operator-framework/deppy/pkg/ext/olm" -) - -func TestConstraints(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Constraints Suite") -} - -func defaultTestEntityList() input.EntityList { - return input.EntityList{ - *input.NewEntity("cool-package-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "2.0.1", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - }), - *input.NewEntity("cool-package-2-0-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-2", - olm.PropertyOLMVersion: "2.0.3", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - }), - *input.NewEntity("cool-package-2-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-2", - olm.PropertyOLMVersion: "2.1.0", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMGVK: "{\"group\":\"my-other-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - }), - *input.NewEntity("cool-package-3-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-3", - olm.PropertyOLMVersion: "3.1.2", - olm.PropertyOLMChannel: "channel-2", - olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - }), - } -} - -// MockQuerier type to mock the entity querier -type MockQuerier struct { - testError error - testEntityList input.EntityList -} - -func (t MockQuerier) Get(_ context.Context, _ deppy.Identifier) (*input.Entity, error) { - return &input.Entity{}, nil -} -func (t MockQuerier) Filter(_ context.Context, filter input.Predicate) (input.EntityList, error) { - if t.testError != nil { - return nil, t.testError - } - ret := input.EntityList{} - for _, entity := range t.testEntityList { - if filter(&entity) { - ret = append(ret, entity) - } - } - return ret, nil -} -func (t MockQuerier) GroupBy(_ context.Context, id input.GroupByFunction) (input.EntityListMap, error) { - if t.testError != nil { - return nil, t.testError - } - ret := input.EntityListMap{} - for _, entity := range t.testEntityList { - keys := id(&entity) - for _, key := range keys { - if _, ok := ret[key]; !ok { - ret[key] = input.EntityList{} - } - ret[key] = append(ret[key], entity) - } - } - return ret, nil -} -func (t MockQuerier) Iterate(_ context.Context, id input.IteratorFunction) error { - if t.testError != nil { - return t.testError - } - return nil -} - -var _ = Describe("Constraints", func() { - Context("requirePackage", func() { - Describe("GetVariables", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - testEntityList: defaultTestEntityList(), - } - }) - // match all - It("returns one satVar entry describing the required package", func() { - satVars, err := olm.RequirePackage("cool-package-1", "<=2.0.2", "channel-1").GetVariables(ctx, mockQuerier) - expectedIdentifier := fmt.Sprintf("require-%s-%s-%s", "cool-package-1", "<=2.0.2", "channel-1") - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - Expect(satVars[0].Identifier().String()).To(Equal(expectedIdentifier)) - Expect(satVars[0].Constraints()).Should(HaveLen(2)) - - // The constraint api is not really transparent - using the String(subject) method to verify they are correct - Expect(satVars[0].Constraints()[0].String("test-pkg")).To(Equal("test-pkg is mandatory")) - Expect(satVars[0].Constraints()[1].String("test-pkg")).To(Equal("test-pkg requires at least one of cool-package-1-entity")) - }) - // package name - It("finds no candidates to satisfy the dependency when package name does not match any entities", func() { - satVars, err := olm.RequirePackage("cool-package-4", "<3.0.0", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("finds no candidates to satisfy the dependency when no entries contain the 'olm.packageName' key", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-3-entity", map[string]string{ - "wrong-key": "cool-package-1", - olm.PropertyOLMVersion: "2.1.2", - olm.PropertyOLMChannel: "channel-1", - }), - } - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-3").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - // version range - It("finds no candidates to satisfy the dependency when no entries match the provided version range", func() { - satVars, err := olm.RequirePackage("cool-package-1", "<=2.0.0", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("finds no candidates to satisfy the dependency when given an invalid version range", func() { - satVars, err := olm.RequirePackage("cool-package-1", "abcdefg", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("finds no candidates to satisfy the dependency when no entries have a valid semver value", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "abcdefg", - olm.PropertyOLMChannel: "channel-1", - }), - } - satVars, err := olm.RequirePackage("cool-package-1", ">=3.0.0", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("finds no candidates to satisfy the dependency when no entries contain the 'olm.version' key", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - "wrong-key": "2.1.2", - olm.PropertyOLMChannel: "channel-1", - }), - } - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-3").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - // channel - It("returns one satVar entry describing no possible dependency candidates when the entry has an empty channel name", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "2.1.2", - olm.PropertyOLMChannel: "", - }), - } - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-3").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("returns one satVar entry describing no candidate when channel requirement is empty", func() { - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - Expect(satVars[0].Constraints()).Should(HaveLen(2)) - Expect(satVars[0].Constraints()[1].String("test-pkg")).To(Equal("test-pkg requires at least one of cool-package-1-entity")) - }) - It("returns one satVar entry describing no possible dependency candidates when no entries match the provided channel", func() { - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-3").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("returns one satVar entry describing no possible dependency candidates when no entries contain the 'olm.channel' key", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "2.1.2", - "wrong-key": "channel-1", - }), - } - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - // entity querier error - It("forwards any error encountered by the entity querier", func() { - mockQuerier.testError = errors.New("oh no") - satVars, err := olm.RequirePackage("cool-package-1", "<=3.0.0", "channel-1").GetVariables(ctx, mockQuerier) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("oh no")) - Expect(satVars).Should(HaveLen(0)) - }) - }) - }) - Context("uniqueness", func() { - Describe("PackageUniqueness", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - testEntityList: defaultTestEntityList(), - } - }) - It("returns a slice of sat.Variable grouped by package name", func() { - satVars, err := olm.PackageUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(3)) - sort.Slice(satVars, func(i, j int) bool { - return satVars[i].Identifier().String() < satVars[j].Identifier().String() - }) - Expect(satVars[0].Identifier().String()).To(Equal("cool-package-1 uniqueness")) - Expect(satVars[0].Constraints()[0].String("test-pkg")).To(Equal("test-pkg permits at most 1 of cool-package-1-entity")) - Expect(satVars[1].Identifier().String()).To(Equal("cool-package-2 uniqueness")) - Expect(satVars[1].Constraints()[0].String("test-pkg")).To(Equal("test-pkg permits at most 1 of cool-package-2-1-entity, cool-package-2-0-entity")) - Expect(satVars[2].Identifier().String()).To(Equal("cool-package-3 uniqueness")) - Expect(satVars[2].Constraints()[0].String("test-pkg")).To(Equal("test-pkg permits at most 1 of cool-package-3-entity")) - }) - It("forwards any error given by the entity querier", func() { - mockQuerier.testError = errors.New("oh no") - satVars, err := olm.PackageUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("oh no")) - Expect(satVars).Should(HaveLen(0)) - }) - It("returns an empty sat.Variable slice when package name key is missing from all entities", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-3-entity", map[string]string{ - "wrong-key": "cool-package-3", - olm.PropertyOLMVersion: "3.1.2", - olm.PropertyOLMChannel: "channel-2", - }), - } - satVars, err := olm.PackageUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - }) - Describe("GVKUniqueness", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - testEntityList: defaultTestEntityList(), - } - }) - It("returns a slice of sat.Variable grouped by group, version, and kind, with constraints ordered by package name", func() { - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(2)) - sort.Slice(satVars, func(i, j int) bool { - return satVars[i].Identifier().String() < satVars[j].Identifier().String() - }) - Expect(satVars[0].Identifier().String()).To(Equal("my-group/my-version/my-kind uniqueness")) - Expect(satVars[0].Constraints()[0].String("foo")).To(Equal("foo permits at most 1 of cool-package-1-entity, cool-package-2-0-entity, cool-package-3-entity")) - Expect(satVars[1].Identifier().String()).To(Equal("my-other-group/my-version/my-kind uniqueness")) - Expect(satVars[1].Constraints()[0].String("foo")).To(Equal("foo permits at most 1 of cool-package-2-1-entity")) - }) - It("forwards any error given by the entity querier", func() { - mockQuerier.testError = errors.New("oh no") - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("oh no")) - Expect(satVars).Should(HaveLen(0)) - }) - It("returns an empty sat.Variable slice when gvk key is missing from all entities", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-3-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-3", - "wrong-key": "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - }), - } - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("returns an empty sat.Variable slice when gvk field is malformed in all entities", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-3-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-3", - olm.PropertyOLMGVK: "abcdefg", - }), - } - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - It("does not panic but returns an empty result set when gvk json is missing fields", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-3-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-3", - olm.PropertyOLMGVK: "{}", - }), - } - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(0)) - }) - }) - }) - Context("dependency", func() { - Describe("PackageDependency", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - testEntityList: defaultTestEntityList(), - } - }) - It("returns one satVar containing an constraint which lists all available dependencies", func() { - satVars, err := olm.PackageDependency("cool-package-2-dep", "cool-package-2", "<=3.0.2").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - Expect(satVars[0].Identifier().String()).To(Equal("cool-package-2-dep")) - Expect(satVars[0].Constraints()).Should(HaveLen(1)) - msg := satVars[0].Constraints()[0].String("test-pkg") - Expect(msg).To(Equal("test-pkg requires at least one of cool-package-2-1-entity, cool-package-2-0-entity")) - }) - It("forwards any error encountered by the entity querier", func() { - mockQuerier.testError = errors.New("oh no") - satVars, err := olm.PackageDependency("cool-package-1-dep", "cool-package-1", ">1.0.0").GetVariables(ctx, mockQuerier) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("oh no")) - Expect(satVars).Should(HaveLen(0)) - }) - }) - Describe("GVKDependency", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - testEntityList: defaultTestEntityList(), - } - }) - It("returns a single satVar which lists all available dependencies based on gvk", func() { - satVars, err := olm.GVKDependency("cool-package-2-dep", "my-group", "my-version", "my-kind").GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - Expect(satVars[0].Identifier().String()).To(Equal("cool-package-2-dep")) - Expect(satVars[0].Constraints()).Should(HaveLen(1)) - msg := satVars[0].Constraints()[0].String("test-pkg") - Expect(msg).To(Equal("test-pkg requires at least one of cool-package-1-entity, cool-package-2-0-entity, cool-package-3-entity")) - }) - It("forwards any error encountered by the entity querier", func() { - mockQuerier.testError = errors.New("oh no") - satVars, err := olm.GVKDependency("cool-package-2-dep", "my-group", "my-version", "my-kind").GetVariables(ctx, mockQuerier) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("oh no")) - Expect(satVars).Should(HaveLen(0)) - }) - }) - }) - Context("byChannelAndVersion", func() { - var ( - ctx context.Context - mockQuerier MockQuerier - ) - BeforeEach(func() { - ctx = context.Background() - mockQuerier = MockQuerier{ - testError: nil, - } - }) - DescribeTable("package name ordering", func(pkg1NameKey string, pkg2NameKey string, matchElements Elements) { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-entity-1", map[string]string{ - pkg1NameKey: "cool-package-1", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - olm.PropertyOLMDefaultChannel: "channel-1", - }), - *input.NewEntity("cool-package-entity-2", map[string]string{ - pkg2NameKey: "cool-package-2", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", - olm.PropertyOLMDefaultChannel: "channel-1", - }), - } - satVars, err := olm.GVKUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - entities := strings.Split(satVars[0].Constraints()[0].String("pkg"), ", ") - Expect(entities).To(MatchAllElementsWithIndex(IndexIdentity, matchElements)) - }, - Entry("orders by packageName when both keys exist", olm.PropertyOLMPackageName, olm.PropertyOLMPackageName, Elements{ - "0": Equal("pkg permits at most 1 of cool-package-entity-1"), - "1": Equal("cool-package-entity-2"), - }), - Entry("orders entity-1 at the bottom when it is missing packageName", "wrong-key", olm.PropertyOLMPackageName, Elements{ - "0": Equal("pkg permits at most 1 of cool-package-entity-2"), - "1": Equal("cool-package-entity-1"), - }), - Entry("orders entity-2 at the bottom when it is missing packageName", olm.PropertyOLMPackageName, "wrong-key", Elements{ - "0": Equal("pkg permits at most 1 of cool-package-entity-1"), - "1": Equal("cool-package-entity-2"), - }), - ) - Describe("channel and version ordering", func() { - It("orders sat vars with identical packageName by channel and version in that order of priority", func() { - mockQuerier.testEntityList = input.EntityList{ - *input.NewEntity("cool-package-1-ch1-1.0-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "1.0.1", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - *input.NewEntity("cool-package-1-ch1-invalid-version-a-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "abcdefg", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - *input.NewEntity("cool-package-1-ch2-versionless-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMChannel: "channel-2", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - *input.NewEntity("cool-package-1-ch1-1.1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "1.1.3", - olm.PropertyOLMChannel: "channel-1", - }), - *input.NewEntity("cool-package-1-ch1-invalid-version-b-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "abcdefg", - olm.PropertyOLMChannel: "channel-1", - }), - *input.NewEntity("cool-package-1-ch2-1.2-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "1.2.3", - olm.PropertyOLMChannel: "channel-2", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - *input.NewEntity("cool-package-1-ch3-1.2-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "1.2.3", - olm.PropertyOLMChannel: "channel-3", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - *input.NewEntity("cool-package-1-channelless-1.1-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMVersion: "1.1.3", - }), - *input.NewEntity("cool-package-1-ch1-versionless-entity", map[string]string{ - olm.PropertyOLMPackageName: "cool-package-1", - olm.PropertyOLMChannel: "channel-1", - olm.PropertyOLMDefaultChannel: "channel-2", - }), - } - satVars, err := olm.PackageUniqueness().GetVariables(ctx, mockQuerier) - Expect(err).NotTo(HaveOccurred()) - Expect(satVars).Should(HaveLen(1)) - entities := strings.Split(satVars[0].Constraints()[0].String("pkg"), ", ") - Expect(entities).To(MatchAllElementsWithIndex(IndexIdentity, Elements{ - // channel-1 first, ordered by version, versionless last - "0": Equal("pkg permits at most 1 of cool-package-1-ch2-1.2-entity"), - "1": Equal("cool-package-1-ch2-versionless-entity"), - "2": Equal("cool-package-1-ch1-1.1-entity"), - "3": Equal("cool-package-1-ch1-1.0-entity"), - "4": Equal("cool-package-1-ch1-invalid-version-a-entity"), - "5": Equal("cool-package-1-ch1-invalid-version-b-entity"), - "6": Equal("cool-package-1-ch1-versionless-entity"), - - "7": Equal("cool-package-1-ch3-1.2-entity"), - // channelless last - "8": Equal("cool-package-1-channelless-1.1-entity"), - })) - }) - }) - }) -})