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"), - })) - }) - }) - }) -})