diff --git a/pkg/envoy/graph.go b/pkg/envoy/graph.go index 7801a08ca..fff411832 100644 --- a/pkg/envoy/graph.go +++ b/pkg/envoy/graph.go @@ -13,7 +13,7 @@ type ( // based on the node properties. // Refer to the documentation for additional details. Graph struct { - nodes []Node + nodes NodeSet // Since it's calculated on the fly, this is all we need invert bool @@ -61,18 +61,16 @@ func (g *Graph) Remove(nn ...Node) { return } - mm := make([]Node, 0, len(g.nodes)) - for _, m := range g.nodes { - for _, n := range nn { - if g.nodesMatch(m, n) && g.canRemove(n) { - goto skip - } + rn := make(NodeSet, 0, len(nn)) + for _, n := range nn { + if g.canRemove(n) { + rn = append(rn, n) } - mm = append(mm, m) - - skip: } - g.nodes = mm + + g.nodes = g.nodes.Remove(rn...) + g.processed = g.processed.Remove(rn...) + g.conflicts = g.conflicts.Remove(rn...) } // FindNode returns all nodes that match the given resource and identifiers diff --git a/pkg/envoy/graph_test.go b/pkg/envoy/graph_test.go new file mode 100644 index 000000000..5a90d0f9d --- /dev/null +++ b/pkg/envoy/graph_test.go @@ -0,0 +1,201 @@ +package envoy + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +type ( + TestNode struct { + rr NodeRelationships + ii NodeIdentifiers + } +) + +func (n *TestNode) Identifiers() NodeIdentifiers { + return n.ii +} + +func (n *TestNode) Matches(resource string, identifiers ...string) bool { + if resource != n.Resource() { + return false + } + + return n.Identifiers().HasAny(identifiers...) +} + +func (n *TestNode) Resource() string { + return "envoy:test:" +} + +func (n *TestNode) Relations() NodeRelationships { + return n.rr +} + +func TestEnvoyGraph_Rel(t *testing.T) { + req := require.New(t) + + t.Run("simple node, no rels", func(t *testing.T) { + g := NewGraph() + + rr := NodeRelationships{} + ii := NodeIdentifiers{"p1"} + n := &TestNode{rr: rr, ii: ii} + g.Add(n) + + cc := g.Children(n) + req.Empty(cc) + + pp := g.Parents(n) + req.Empty(pp) + }) + + t.Run("node with child and parent nodes", func(t *testing.T) { + g := NewGraph() + + // The child node + rr1 := NodeRelationships{} + ii1 := NodeIdentifiers{"c1"} + n1 := &TestNode{rr: rr1, ii: ii1} + g.Add(n1) + + // The middle node + rr2 := NodeRelationships{"envoy:test:": NodeIdentifiers{"c1"}} + ii2 := NodeIdentifiers{"n"} + n := &TestNode{rr: rr2, ii: ii2} + g.Add(n) + + // The parent node + rr3 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n"}} + ii3 := NodeIdentifiers{"p1"} + n3 := &TestNode{rr: rr3, ii: ii3} + g.Add(n3) + + cc := g.Children(n) + req.Len(cc, 1) + req.Equal(n1, cc[0]) + + pp := g.Parents(n) + req.Len(pp, 1) + req.Equal(n3, pp[0]) + }) + + t.Run("(inverted) node with child and parent nodes", func(t *testing.T) { + g := NewGraph() + g.Invert() + + // The child node + rr1 := NodeRelationships{} + ii1 := NodeIdentifiers{"c1"} + n1 := &TestNode{rr: rr1, ii: ii1} + g.Add(n1) + + // The middle node + rr2 := NodeRelationships{"envoy:test:": NodeIdentifiers{"c1"}} + ii2 := NodeIdentifiers{"n"} + n := &TestNode{rr: rr2, ii: ii2} + g.Add(n) + + // The parent node + rr3 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n"}} + ii3 := NodeIdentifiers{"p1"} + n3 := &TestNode{rr: rr3, ii: ii3} + g.Add(n3) + + cc := g.Children(n) + req.Len(cc, 1) + req.Equal(n3, cc[0]) + + pp := g.Parents(n) + req.Len(pp, 1) + req.Equal(n1, pp[0]) + }) +} + +func TestEnvoyGraph_DepResolution(t *testing.T) { + req := require.New(t) + ctx := context.Background() + + t.Run("simple acyclic linear graph; (n1) => (n2) => (n3)", func(t *testing.T) { + g := NewGraph() + + rr1 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n2"}} + ii1 := NodeIdentifiers{"n1"} + n1 := &TestNode{rr: rr1, ii: ii1} + g.Add(n1) + + rr2 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n3"}} + ii2 := NodeIdentifiers{"n2"} + n2 := &TestNode{rr: rr2, ii: ii2} + g.Add(n2) + + rr3 := NodeRelationships{} + ii3 := NodeIdentifiers{"n3"} + n3 := &TestNode{rr: rr3, ii: ii3} + g.Add(n3) + + // 1. n1 since it has no parent nodes + n, pp, cc, err := g.Next(ctx) + req.Equal(n1, n) + req.NoError(err) + req.NotEmpty(cc) + req.Empty(pp) + + // 2. n2 since its parents are resolved + n, pp, cc, err = g.Next(ctx) + req.Equal(n2, n) + req.NoError(err) + req.NotEmpty(cc) + req.NotEmpty(pp) + + // 2. n3 since its parents are resolved + n, pp, cc, err = g.Next(ctx) + req.Equal(n3, n) + req.NoError(err) + req.Empty(cc) + req.NotEmpty(pp) + }) +} + +func TestEnvoyGraph_GarbageCollector(t *testing.T) { + req := require.New(t) + ctx := context.Background() + + t.Run("simple acyclic linear graph; (n1) => (n2) => (n3)", func(t *testing.T) { + g := NewGraph() + + rr1 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n2"}} + ii1 := NodeIdentifiers{"n1"} + n1 := &TestNode{rr: rr1, ii: ii1} + g.Add(n1) + + rr2 := NodeRelationships{"envoy:test:": NodeIdentifiers{"n3"}} + ii2 := NodeIdentifiers{"n2"} + n2 := &TestNode{rr: rr2, ii: ii2} + g.Add(n2) + + rr3 := NodeRelationships{} + ii3 := NodeIdentifiers{"n3"} + n3 := &TestNode{rr: rr3, ii: ii3} + g.Add(n3) + + // n1 marked as processed; no garbage + g.Next(ctx) + req.Len(g.nodes, 3) + req.Len(g.processed, 1) + req.Equal(g.processed[0], n1) + + // n2 marked as processed; garbage in nodes, processed + g.Next(ctx) + req.Len(g.nodes, 2) + req.Len(g.processed, 1) + req.Equal(g.processed[0], n2) + + // all nodes processed; resetting graph + g.Next(ctx) + req.Len(g.nodes, 0) + req.Len(g.processed, 0) + }) +} diff --git a/pkg/envoy/node.go b/pkg/envoy/node.go index e2bdb8898..e71698951 100644 --- a/pkg/envoy/node.go +++ b/pkg/envoy/node.go @@ -92,3 +92,24 @@ func (ss NodeSet) Has(n Node) bool { } return has } + +func (ss NodeSet) Remove(nn ...Node) NodeSet { + mm := make(NodeSet, 0, len(ss)) + + if len(nn) <= 0 { + return ss + } + + for _, s := range ss { + for _, n := range nn { + if s.Matches(n.Resource(), n.Identifiers()...) { + goto skip + } + } + mm = append(mm, s) + + skip: + } + + return mm +}