diff --git a/examples/color_policy/integration_test.go b/examples/color_policy/integration_test.go index 8d87b77..3a3b75c 100644 --- a/examples/color_policy/integration_test.go +++ b/examples/color_policy/integration_test.go @@ -151,11 +151,13 @@ func TestKuadrantMergeBasedOnTopology(t *testing.T) { machinery.SaveToOutputDir(t, topology.ToDot(), "../../tests/out", ".dot") - gateways := topology.Targetables(func(o machinery.Object) bool { + targetables := topology.Targetables() + + gateways := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Gateway) return ok }) - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + httpRouteRules := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.HTTPRouteRule) return ok }) @@ -163,7 +165,7 @@ func TestKuadrantMergeBasedOnTopology(t *testing.T) { effectivePoliciesByPath := make(map[string]ColorPolicy) for _, httpRouteRule := range httpRouteRules { - for _, path := range topology.Paths(gateways[0], httpRouteRule) { + for _, path := range targetables.Paths(gateways[0], httpRouteRule) { // Gather all policies in the path sorted from the least specific (gateway) to the most specific (httprouterule) // Since in this example there are no targetables with more than one policy attached to it, we can safely just // flat the slices of policies; otherwise we would need to ensure that the policies at the same level are sorted diff --git a/examples/json_patch/integration_test.go b/examples/json_patch/integration_test.go index 88796d3..e60471e 100644 --- a/examples/json_patch/integration_test.go +++ b/examples/json_patch/integration_test.go @@ -121,11 +121,13 @@ func TestJSONPatchMergeBasedOnTopology(t *testing.T) { machinery.SaveToOutputDir(t, topology.ToDot(), "../../tests/out", ".dot") - gateways := topology.Targetables(func(o machinery.Object) bool { + targetables := topology.Targetables() + + gateways := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Gateway) return ok }) - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + httpRouteRules := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.HTTPRouteRule) return ok }) @@ -133,7 +135,7 @@ func TestJSONPatchMergeBasedOnTopology(t *testing.T) { effectivePoliciesByPath := make(map[string]ColorPolicy) for _, httpRouteRule := range httpRouteRules { - for _, path := range topology.Paths(gateways[0], httpRouteRule) { + for _, path := range targetables.Paths(gateways[0], httpRouteRule) { // Gather all policies in the path sorted from the least specific (gateway) to the most specific (httprouterule) // Since in this example there are no targetables with more than one policy attached to it, we can safely just // flat the slices of policies; otherwise we would need to ensure that the policies at the same level are sorted diff --git a/examples/kuadrant/main.go b/examples/kuadrant/main.go index 82c5f1a..d493184 100644 --- a/examples/kuadrant/main.go +++ b/examples/kuadrant/main.go @@ -80,18 +80,20 @@ func reconcile(eventType controller.EventType, oldObj, newObj controller.Runtime // update the topology file saveTopologyToFile(topology) + targetables := topology.Targetables() + // reconcile policies - gateways := topology.Targetables(func(o machinery.Object) bool { + gateways := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Gateway) return ok }) - listeners := topology.Targetables(func(o machinery.Object) bool { + listeners := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Listener) return ok }) - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + httpRouteRules := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.HTTPRouteRule) return ok }) @@ -99,7 +101,7 @@ func reconcile(eventType controller.EventType, oldObj, newObj controller.Runtime for _, gateway := range gateways { // reconcile Gateway -> Listener policies for _, listener := range listeners { - paths := topology.Paths(gateway, listener) + paths := targetables.Paths(gateway, listener) for i := range paths { effectivePolicyForPath[*kuadrantv1alpha2.DNSPolicy](paths[i]) effectivePolicyForPath[*kuadrantv1alpha2.TLSPolicy](paths[i]) @@ -108,7 +110,7 @@ func reconcile(eventType controller.EventType, oldObj, newObj controller.Runtime // reconcile Gateway -> HTTPRouteRule policies for _, httpRouteRule := range httpRouteRules { - paths := topology.Paths(gateway, httpRouteRule) + paths := targetables.Paths(gateway, httpRouteRule) for i := range paths { effectivePolicyForPath[*kuadrantv1beta3.AuthPolicy](paths[i]) effectivePolicyForPath[*kuadrantv1beta3.RateLimitPolicy](paths[i]) diff --git a/machinery/gateway_api_topology_test.go b/machinery/gateway_api_topology_test.go index 10eafc7..d8240a2 100644 --- a/machinery/gateway_api_topology_test.go +++ b/machinery/gateway_api_topology_test.go @@ -95,8 +95,8 @@ func TestGatewayAPITopology(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] @@ -245,8 +245,8 @@ func TestGatewayAPITopologyWithSectionNames(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] diff --git a/machinery/test_helper.go b/machinery/test_helper.go index 0750654..046b72d 100644 --- a/machinery/test_helper.go +++ b/machinery/test_helper.go @@ -28,14 +28,14 @@ func SaveToOutputDir(t *testing.T, out *bytes.Buffer, outDir, ext string) { } } -func linksFromNode(topology *Topology, node Targetable, edges map[string][]string) { - if _, ok := edges[node.GetName()]; ok { +func linksFromTargetable(topology *Topology, targetable Targetable, edges map[string][]string) { + if _, ok := edges[targetable.GetName()]; ok { return } - children := topology.Children(node) - edges[node.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) + children := topology.Targetables().Children(targetable) + edges[targetable.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) for _, child := range children { - linksFromNode(topology, child, edges) + linksFromTargetable(topology, child, edges) } } diff --git a/machinery/topology.go b/machinery/topology.go index ecd5048..b210d51 100644 --- a/machinery/topology.go +++ b/machinery/topology.go @@ -13,10 +13,10 @@ import ( ) type TopologyOptions struct { - Objects []Object Targetables []Targetable - Links []LinkFunc Policies []Policy + Objects []Object + Links []LinkFunc } type LinkFunc struct { @@ -27,43 +27,46 @@ type LinkFunc struct { type TopologyOptionsFunc func(*TopologyOptions) -// WithObjects adds generic objects to the options to initialize a new topology. -// Do not use this function to add targetables or policies. -// Use WithLinks to define the relationships between objects of any kind. -func WithObjects[T Object](objects ...T) TopologyOptionsFunc { +// WithTargetables adds targetables to the options to initialize a new topology. +func WithTargetables[T Targetable](targetables ...T) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Objects = append(o.Objects, lo.Map(objects, func(object T, _ int) Object { - return object + o.Targetables = append(o.Targetables, lo.Map(targetables, func(targetable T, _ int) Targetable { + return targetable })...) } } -// WithTargetables adds targetables to the options to initialize a new topology. -func WithTargetables[T Targetable](targetables ...T) TopologyOptionsFunc { +// WithPolicies adds policies to the options to initialize a new topology. +func WithPolicies[T Policy](policies ...T) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Targetables = append(o.Targetables, lo.Map(targetables, func(targetable T, _ int) Targetable { - return targetable + o.Policies = append(o.Policies, lo.Map(policies, func(policy T, _ int) Policy { + return policy })...) } } -// WithLinks adds link functions to the options to initialize a new topology. -func WithLinks(links ...LinkFunc) TopologyOptionsFunc { +// WithObjects adds generic objects to the options to initialize a new topology. +// Do not use this function to add targetables or policies. +// Use WithLinks to define the relationships between objects of any kind. +func WithObjects[T Object](objects ...T) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Links = append(o.Links, links...) + o.Objects = append(o.Objects, lo.Map(objects, func(object T, _ int) Object { + return object + })...) } } -// WithPolicies adds policies to the options to initialize a new topology. -func WithPolicies(policies ...Policy) TopologyOptionsFunc { +// WithLinks adds link functions to the options to initialize a new topology. +func WithLinks(links ...LinkFunc) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Policies = append(o.Policies, policies...) + o.Links = append(o.Links, links...) } } -// NewTopology returns a network of targetable resources and attached policies. +// NewTopology returns a network of targetable resources, attached policies, and other kinds of objects. // The topology is represented as a directed acyclic graph (DAG) with the structure given by link functions. -// The targetables, policies and link functions are provided as options. +// The links between policies to targteables are inferred from the policies' target references. +// The targetables, policies, objects and link functions are provided as options. func NewTopology(options ...TopologyOptionsFunc) *Topology { o := &TopologyOptions{} for _, f := range options { @@ -94,6 +97,7 @@ func NewTopology(options ...TopologyOptionsFunc) *Topology { addTargetablesToGraph(graph, targetables) linkables := append(o.Objects, lo.Map(targetables, AsObject[Targetable])...) + linkables = append(linkables, lo.Map(policies, AsObject[Policy])...) for _, link := range o.Links { children := lo.Filter(linkables, func(l Object, _ int) bool { @@ -112,6 +116,7 @@ func NewTopology(options ...TopologyOptionsFunc) *Topology { return &Topology{ graph: graph, + objects: lo.SliceToMap(o.Objects, associateURL[Object]), targetables: lo.SliceToMap(targetables, associateURL[Targetable]), policies: lo.SliceToMap(policies, associateURL[Policy]), } @@ -122,29 +127,128 @@ type Topology struct { graph *cgraph.Graph targetables map[string]Targetable policies map[string]Policy + objects map[string]Object } -type FilterFunc func(Object) bool +// Targetables returns all targetable nodes in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Targetables() *collection[Targetable] { + return &collection[Targetable]{ + topology: t, + items: t.targetables, + } +} -// Targetables returns all targetable nodes of a given kind in the topology. -func (t *Topology) Targetables(filters ...FilterFunc) []Targetable { - return lo.Filter(lo.Values(t.targetables), func(targetable Targetable, _ int) bool { - o := targetable.(Object) - for _, f := range filters { - if !f(o) { - return false +// Policies returns all policies in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Policies() *collection[Policy] { + return &collection[Policy]{ + topology: t, + items: t.policies, + } +} + +// Objects returns all non-targetable, non-policy object nodes in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Objects() *collection[Object] { + return &collection[Object]{ + topology: t, + items: t.objects, + } +} + +func (t *Topology) ToDot() *bytes.Buffer { + gz := graphviz.New() + var buf bytes.Buffer + gz.Render(t.graph, "dot", &buf) + return &buf +} + +func addObjectsToGraph[T Object](graph *cgraph.Graph, objects []T) []*cgraph.Node { + return lo.Map(objects, func(object T, _ int) *cgraph.Node { + name := strings.TrimPrefix(namespacedName(object.GetNamespace(), object.GetName()), string(k8stypes.Separator)) + n, _ := graph.CreateNode(string(object.GetURL())) + n.SetLabel(fmt.Sprintf("%s\\n%s", object.GroupVersionKind().Kind, name)) + n.SetShape(cgraph.EllipseShape) + return n + }) +} + +func addTargetablesToGraph[T Targetable](graph *cgraph.Graph, targetables []T) { + for _, node := range addObjectsToGraph(graph, targetables) { + node.SetShape(cgraph.BoxShape) + node.SetStyle(cgraph.FilledNodeStyle) + node.SetFillColor("#e5e5e5") + } +} + +func addPoliciesToGraph[T Policy](graph *cgraph.Graph, policies []T) { + for i, policyNode := range addObjectsToGraph(graph, policies) { + policyNode.SetShape(cgraph.NoteShape) + policyNode.SetStyle(cgraph.DashedNodeStyle) + // Policy -> Target edges + for _, targetRef := range policies[i].GetTargetRefs() { + targetNode, _ := graph.Node(string(targetRef.GetURL())) + if targetNode != nil { + edge, _ := graph.CreateEdge("Policy -> Target", policyNode, targetNode) + edge.SetStyle(cgraph.DashedEdgeStyle) } } - return true - }) + } +} + +func addEdgeToGraph(graph *cgraph.Graph, name string, parent, child Object) { + p, _ := graph.Node(string(parent.GetURL())) + c, _ := graph.Node(string(child.GetURL())) + if p != nil && c != nil { + graph.CreateEdge(name, p, c) + } +} + +func associateURL[T Object](obj T) (string, T) { + return obj.GetURL(), obj +} + +type collection[T Object] struct { + topology *Topology + items map[string]T +} + +type FilterFunc func(Object) bool + +// Targetables returns all targetable nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Targetables() *collection[Targetable] { + return &collection[Targetable]{ + topology: c.topology, + items: c.topology.targetables, + } +} + +// Policies returns all policies in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Policies() *collection[Policy] { + return &collection[Policy]{ + topology: c.topology, + items: c.topology.policies, + } +} + +// Objects returns all non-targetable, non-policy object nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Objects() *collection[Object] { + return &collection[Object]{ + topology: c.topology, + items: c.topology.objects, + } } -// Policies returns all policies of a given kind in the topology. -func (t *Topology) Policies(filters ...FilterFunc) []Policy { - return lo.Filter(lo.Values(t.policies), func(policy Policy, _ int) bool { - o := policy.(Object) +// List returns all items nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Items(filters ...FilterFunc) []T { + return lo.Filter(lo.Values(c.items), func(item T, _ int) bool { for _, f := range filters { - if !f(o) { + if !f(item) { return false } } @@ -152,136 +256,85 @@ func (t *Topology) Policies(filters ...FilterFunc) []Policy { }) } -// Roots returns all targetables that have no parents in the topology. -func (t *Topology) Roots() []Targetable { - return lo.Filter(lo.Values(t.targetables), func(targetable Targetable, _ int) bool { - return len(t.Parents(targetable)) == 0 +// Roots returns all items that have no parents in the collection. +func (c *collection[T]) Roots() []T { + return lo.Filter(lo.Values(c.items), func(item T, _ int) bool { + return len(c.Parents(item)) == 0 }) } -// Parents returns all parents of a given targetable in the topology. -func (t *Topology) Parents(targetable Targetable) []Targetable { - var parents []Targetable - n, err := t.graph.Node(targetable.GetURL()) +// Parents returns all parents of a given item in the collection. +func (c *collection[T]) Parents(item T) []T { + var parents []T + n, err := c.topology.graph.Node(item.GetURL()) if err != nil { return nil } - edge := t.graph.FirstIn(n) + edge := c.topology.graph.FirstIn(n) for { if edge == nil { break } - _, ok := t.targetables[edge.Node().Name()] + _, ok := c.items[edge.Node().Name()] if ok { - parents = append(parents, t.targetables[edge.Node().Name()]) + parents = append(parents, c.items[edge.Node().Name()]) } - edge = t.graph.NextIn(edge) + edge = c.topology.graph.NextIn(edge) } return parents } -// Children returns all children of a given targetable in the topology. -func (t *Topology) Children(targetable Targetable) []Targetable { - var children []Targetable - n, err := t.graph.Node(targetable.GetURL()) +// Children returns all children of a given item in the collection. +func (c *collection[T]) Children(item T) []T { + var children []T + n, err := c.topology.graph.Node(item.GetURL()) if err != nil { return nil } - edge := t.graph.FirstOut(n) + edge := c.topology.graph.FirstOut(n) for { if edge == nil { break } - _, ok := t.targetables[edge.Node().Name()] + _, ok := c.items[edge.Node().Name()] if ok { - children = append(children, t.targetables[edge.Node().Name()]) + children = append(children, c.items[edge.Node().Name()]) } - edge = t.graph.NextOut(edge) + edge = c.topology.graph.NextOut(edge) } return children } -// Paths returns all paths from a source targetable to a destination targetable in the topology. +// Paths returns all paths from a source item to a destination item in the collection. // The order of the elements in the inner slices represents a path from the source to the destination. -func (t *Topology) Paths(from, to Targetable) [][]Targetable { - if from == nil || to == nil { +func (c *collection[T]) Paths(from, to T) [][]T { + if &from == nil || &to == nil { return nil } - var paths [][]Targetable - var path []Targetable + var paths [][]T + var path []T visited := make(map[string]bool) - t.dfs(from, to, path, &paths, visited) + c.dfs(from, to, path, &paths, visited) return paths } -func (t *Topology) ToDot() *bytes.Buffer { - gz := graphviz.New() - var buf bytes.Buffer - gz.Render(t.graph, "dot", &buf) - return &buf -} - -func (t *Topology) dfs(current, to Targetable, path []Targetable, paths *[][]Targetable, visited map[string]bool) { +// dfs performs a depth-first search to find all paths from a source item to a destination item in the collection. +func (c *collection[T]) dfs(current, to T, path []T, paths *[][]T, visited map[string]bool) { currentURL := current.GetURL() if visited[currentURL] { return } - path = append(path, t.targetables[currentURL]) + path = append(path, c.items[currentURL]) visited[currentURL] = true if currentURL == to.GetURL() { - pathCopy := make([]Targetable, len(path)) + pathCopy := make([]T, len(path)) copy(pathCopy, path) *paths = append(*paths, pathCopy) } else { - for _, child := range t.Children(current) { - t.dfs(child, to, path, paths, visited) + for _, child := range c.Children(current) { + c.dfs(child, to, path, paths, visited) } } path = path[:len(path)-1] visited[currentURL] = false } - -func associateURL[T Object](obj T) (string, T) { - return obj.GetURL(), obj -} - -func addObjectsToGraph[T Object](graph *cgraph.Graph, objects []T) []*cgraph.Node { - return lo.Map(objects, func(object T, _ int) *cgraph.Node { - name := strings.TrimPrefix(namespacedName(object.GetNamespace(), object.GetName()), string(k8stypes.Separator)) - n, _ := graph.CreateNode(string(object.GetURL())) - n.SetLabel(fmt.Sprintf("%s\\n%s", object.GroupVersionKind().Kind, name)) - n.SetShape(cgraph.EllipseShape) - return n - }) -} - -func addTargetablesToGraph[T Targetable](graph *cgraph.Graph, targetables []T) { - for _, node := range addObjectsToGraph(graph, targetables) { - node.SetShape(cgraph.BoxShape) - node.SetStyle(cgraph.FilledNodeStyle) - node.SetFillColor("#e5e5e5") - } -} - -func addPoliciesToGraph[T Policy](graph *cgraph.Graph, policies []T) { - for i, policyNode := range addObjectsToGraph(graph, policies) { - policyNode.SetShape(cgraph.NoteShape) - policyNode.SetStyle(cgraph.DashedNodeStyle) - // Policy -> Target edges - for _, targetRef := range policies[i].GetTargetRefs() { - targetNode, _ := graph.Node(string(targetRef.GetURL())) - if targetNode != nil { - edge, _ := graph.CreateEdge("Policy -> Target", policyNode, targetNode) - edge.SetStyle(cgraph.DashedEdgeStyle) - } - } - } -} - -func addEdgeToGraph(graph *cgraph.Graph, name string, parent, child Object) { - p, _ := graph.Node(string(parent.GetURL())) - c, _ := graph.Node(string(child.GetURL())) - if p != nil && c != nil { - graph.CreateEdge(name, p, c) - } -} diff --git a/machinery/topology_test.go b/machinery/topology_test.go index 1eab822..bfa37de 100644 --- a/machinery/topology_test.go +++ b/machinery/topology_test.go @@ -38,7 +38,7 @@ func TestTopologyRoots(t *testing.T) { }), ), ) - roots := topology.Roots() + roots := topology.Targetables().Roots() if expected := len(apples); len(roots) != expected { t.Errorf("expected %d roots, got %d", expected, len(roots)) } @@ -71,7 +71,7 @@ func TestTopologyParents(t *testing.T) { ), ) // orange-1 - parents := topology.Parents(orange1) + parents := topology.Targetables().Parents(orange1) if expected := 2; len(parents) != expected { t.Errorf("expected %d parent, got %d", expected, len(parents)) } @@ -83,7 +83,7 @@ func TestTopologyParents(t *testing.T) { t.Errorf("expected parent %s not found", apple2.GetURL()) } // orange-2 - parents = topology.Parents(orange2) + parents = topology.Targetables().Parents(orange2) if expected := 1; len(parents) != expected { t.Errorf("expected %d parent, got %d", expected, len(parents)) } @@ -114,7 +114,7 @@ func TestTopologyChildren(t *testing.T) { ), ) // apple-1 - children := topology.Children(apple1) + children := topology.Targetables().Children(apple1) if expected := 1; len(children) != expected { t.Errorf("expected %d child, got %d", expected, len(children)) } @@ -123,7 +123,7 @@ func TestTopologyChildren(t *testing.T) { t.Errorf("expected child %s not found", orange1.GetURL()) } // apple-2 - children = topology.Children(apple2) + children = topology.Targetables().Children(apple2) if expected := 2; len(children) != expected { t.Errorf("expected %d child, got %d", expected, len(children)) } @@ -203,7 +203,7 @@ func TestTopologyPaths(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - paths := topology.Paths(tc.from, tc.to) + paths := topology.Targetables().Paths(tc.from, tc.to) if len(paths) != len(tc.expectedPaths) { t.Errorf("expected %d paths, got %d", len(tc.expectedPaths), len(paths)) } @@ -297,8 +297,8 @@ func TestFruitTopology(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] @@ -355,8 +355,8 @@ func TestTopologyWithGenericObjects(t *testing.T) { } links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := expectedLinks[from]