diff --git a/machinery/test_helper.go b/machinery/test_helper.go index 8621bc6..a299f79 100644 --- a/machinery/test_helper.go +++ b/machinery/test_helper.go @@ -38,6 +38,17 @@ func linksFromTargetable(topology *Topology, targetable Targetable, edges map[st } } +func linksFromAll(topology *Topology, obj Object, edges map[string][]string) { + if _, ok := edges[obj.GetName()]; ok { + return + } + children := topology.All().Children(obj) + edges[obj.GetName()] = lo.Map(children, func(child Object, _ int) string { return child.GetName() }) + for _, child := range children { + linksFromAll(topology, child, edges) + } +} + const TestGroupName = "example.test" type Apple struct { diff --git a/machinery/topology.go b/machinery/topology.go index 9954899..da65101 100644 --- a/machinery/topology.go +++ b/machinery/topology.go @@ -168,6 +168,22 @@ func (t *Topology) Objects() *collection[Object] { } } +// All returns all object nodes in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) All() *collection[Object] { + allObjects := t.objects + for k, v := range t.targetables { + allObjects[k] = v + } + for k, v := range t.policies { + allObjects[k] = v + } + return &collection[Object]{ + topology: t, + items: allObjects, + } +} + func (t *Topology) ToDot() string { return t.graph.String() } diff --git a/machinery/topology_test.go b/machinery/topology_test.go index 2c74cc1..841cd2c 100644 --- a/machinery/topology_test.go +++ b/machinery/topology_test.go @@ -377,21 +377,29 @@ func TestTopologyWithRuntimeObjects(t *testing.T) { } expectedLinks := map[string][]string{ - "apple-1": {"orange-1", "orange-2"}, - "info-1": {"apple-1"}, - "info-2": {"orange-1"}, + "apple-1": {"orange-1", "orange-2"}, + "orange-1": {}, + "orange-2": {}, } links := make(map[string][]string) for _, root := range topology.Targetables().Roots() { linksFromTargetable(topology, root, links) } - for from, tos := range links { - expectedTos := expectedLinks[from] + + if len(links) != len(expectedLinks) { + t.Errorf("expected links length to be %v, got %v", len(expectedLinks), len(links)) + } + + for expectedFrom, expectedTos := range expectedLinks { + tos, ok := links[expectedFrom] + if !ok { + t.Errorf("expected root for %v, got none", expectedFrom) + } slices.Sort(expectedTos) slices.Sort(tos) if !slices.Equal(expectedTos, tos) { - t.Errorf("expected links from %s to be %v, got %v", from, expectedTos, tos) + t.Errorf("expected links from %s to be %v, got %v", expectedFrom, expectedTos, tos) } } @@ -502,3 +510,176 @@ func TestTopologyHasNoLoops(t *testing.T) { t.Errorf("Expected no error, got: %s", err.Error()) } } + +func TestTopologyAll(t *testing.T) { + objects := []*Info{ + {Name: "info-1", Ref: "apple.example.test:apple-1"}, + {Name: "info-2", Ref: "orange.example.test:my-namespace/orange-1"}, + } + apples := []*Apple{{Name: "apple-1"}} + oranges := []*Orange{ + {Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + {Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + } + policies := []Policy{ + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-1" + policy.Spec.TargetRef.Kind = "Apple" + policy.Spec.TargetRef.Name = "apple-1" + }), + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef.Kind = "Orange" + policy.Spec.TargetRef.Name = "orange-1" + }), + } + + topology, err := NewTopology( + WithObjects(objects...), + WithTargetables(apples...), + WithTargetables(oranges...), + WithPolicies(policies...), + WithLinks( + LinkApplesToOranges(apples), + LinkInfoFrom("Apple", lo.Map(apples, AsObject[*Apple])), + LinkInfoFrom("Orange", lo.Map(oranges, AsObject[*Orange])), + ), + ) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + SaveToOutputDir(t, topology.ToDot(), "../tests/out", ".dot") + + expectedLinks := map[string][]string{ + "policy-1": {"apple-1"}, + "policy-2": {"orange-1"}, + "apple-1": {"orange-1", "orange-2", "info-1"}, + "orange-1": {"info-2"}, + "orange-2": {}, + "info-1": {}, + "info-2": {}, + } + + links := make(map[string][]string) + for _, root := range topology.All().Roots() { + linksFromAll(topology, root, links) + } + + if len(links) != len(expectedLinks) { + t.Errorf("expected links length to be %v, got %v", len(expectedLinks), len(links)) + } + + for expectedFrom, expectedTos := range expectedLinks { + tos, ok := links[expectedFrom] + if !ok { + t.Errorf("expected root for %v, got none", expectedFrom) + } + slices.Sort(expectedTos) + slices.Sort(tos) + if !slices.Equal(expectedTos, tos) { + t.Errorf("expected links from %s to be %v, got %v", expectedFrom, expectedTos, tos) + } + } +} + +func TestTopologyAllPaths(t *testing.T) { + objects := []*Info{ + {Name: "info-1", Ref: "apple.example.test:apple-1"}, + {Name: "info-2", Ref: "orange.example.test:my-namespace/orange-1"}, + } + apples := []*Apple{{Name: "apple-1"}} + oranges := []*Orange{ + {Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + {Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + } + policies := []Policy{ + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-1" + policy.Spec.TargetRef.Kind = "Apple" + policy.Spec.TargetRef.Name = "apple-1" + }), + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef.Kind = "Orange" + policy.Spec.TargetRef.Name = "orange-1" + }), + } + + topology, err := NewTopology( + WithObjects(objects...), + WithTargetables(apples...), + WithTargetables(oranges...), + WithPolicies(policies...), + WithLinks( + LinkApplesToOranges(apples), + LinkInfoFrom("Apple", lo.Map(apples, AsObject[*Apple])), + LinkInfoFrom("Orange", lo.Map(oranges, AsObject[*Orange])), + ), + ) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + SaveToOutputDir(t, topology.ToDot(), "../tests/out", ".dot") + + testCases := []struct { + name string + from Object + to Object + expectedPaths [][]Object + }{ + { + name: "policy to targetable", + from: policies[0], + to: apples[0], + expectedPaths: [][]Object{ + {policies[0], apples[0]}, + }, + }, + { + name: "targetable to targetable", + from: apples[0], + to: oranges[0], + expectedPaths: [][]Object{ + {apples[0], oranges[0]}, + }, + }, + { + name: "targetable to object", + from: oranges[0], + to: objects[1], + expectedPaths: [][]Object{ + {oranges[0], objects[1]}, + }, + }, + { + name: "policy to object", + from: policies[0], + to: objects[1], + expectedPaths: [][]Object{ + {policies[0], apples[0], oranges[0], objects[1]}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + paths := topology.All().Paths(tc.from, tc.to) + if len(paths) != len(tc.expectedPaths) { + t.Errorf("expected %d paths, got %d", len(tc.expectedPaths), len(paths)) + } + expectedPaths := lo.Map(tc.expectedPaths, func(expectedPath []Object, _ int) string { + return strings.Join(lo.Map(expectedPath, MapObjectToLocatorFunc), "→") + }) + for _, path := range paths { + pathString := strings.Join(lo.Map(path, MapObjectToLocatorFunc), "→") + if !lo.Contains(expectedPaths, pathString) { + t.Errorf("expected path %v not found", pathString) + } + } + }) + } +} diff --git a/machinery/types.go b/machinery/types.go index fce2efe..f0967b8 100644 --- a/machinery/types.go +++ b/machinery/types.go @@ -18,6 +18,10 @@ type Object interface { GetLocator() string } +func MapObjectToLocatorFunc(t Object, _ int) string { + return t.GetLocator() +} + func LocatorFromObject(obj Object) string { name := strings.TrimPrefix(namespacedName(obj.GetNamespace(), obj.GetName()), string(k8stypes.Separator)) return fmt.Sprintf("%s%s%s", strings.ToLower(obj.GroupVersionKind().GroupKind().String()), string(kindNameLocatorSeparator), name)