-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright 2020 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// This package provides a graph data struture | ||
// and graph functionality using ObjMetadata as | ||
// vertices in the graph. | ||
package graph | ||
|
||
import ( | ||
"fmt" | ||
|
||
"sigs.k8s.io/cli-utils/pkg/object" | ||
) | ||
|
||
// Graph is contains a directed set of edges, implemented as | ||
// an adjacency list (map key is "from" vertex, slice are "to" | ||
// vertices). | ||
type Graph struct { | ||
// map "from" vertex -> list of "to" vertices | ||
edges map[object.ObjMetadata][]object.ObjMetadata | ||
} | ||
|
||
// Edge encapsulates a pair of vertices describing a | ||
// directed edge. | ||
type Edge struct { | ||
From object.ObjMetadata | ||
To object.ObjMetadata | ||
} | ||
|
||
// New returns a pointer to an empty Graph data structure. | ||
func New() *Graph { | ||
g := &Graph{} | ||
g.edges = make(map[object.ObjMetadata][]object.ObjMetadata) | ||
return g | ||
} | ||
|
||
// AddVertex adds an ObjMetadata vertex to the graph, with | ||
// an initial empty set of edges from added vertex. | ||
func (g *Graph) AddVertex(v object.ObjMetadata) { | ||
if _, exists := g.edges[v]; !exists { | ||
g.edges[v] = []object.ObjMetadata{} | ||
} | ||
} | ||
|
||
// AddEdge adds a edge from one ObjMetadata vertex to another. The | ||
// direction of the edge is "from" -> "to". | ||
func (g *Graph) AddEdge(from object.ObjMetadata, to object.ObjMetadata) { | ||
// Add "from" vertex if it doesn't already exist. | ||
if _, exists := g.edges[from]; !exists { | ||
g.edges[from] = []object.ObjMetadata{} | ||
} | ||
// Add "to" vertex if it doesn't already exist. | ||
if _, exists := g.edges[to]; !exists { | ||
g.edges[to] = []object.ObjMetadata{} | ||
} | ||
// Add edge "from" -> "to" if it doesn't already exist | ||
// into the adjacency list. | ||
if !g.isAdjacent(from, to) { | ||
g.edges[from] = append(g.edges[from], to) | ||
} | ||
} | ||
|
||
// GetEdges returns the slice of vertex pairs which are | ||
// the directed edges of the graph. | ||
func (g *Graph) GetEdges() []Edge { | ||
edges := []Edge{} | ||
for from, toList := range g.edges { | ||
for _, to := range toList { | ||
edge := Edge{From: from, To: to} | ||
edges = append(edges, edge) | ||
} | ||
} | ||
return edges | ||
} | ||
|
||
// isAdjacent returns true if an edge "from" vertex -> "to" vertex exists; | ||
// false otherwise. | ||
func (g *Graph) isAdjacent(from object.ObjMetadata, to object.ObjMetadata) bool { | ||
// If "from" vertex does not exist, it is impossible edge exists; return false. | ||
if _, exists := g.edges[from]; !exists { | ||
return false | ||
} | ||
// Iterate through adjacency list to see if "to" vertex is adjacent. | ||
for _, vertex := range g.edges[from] { | ||
if vertex == to { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// Size returns the number of vertices in the graph. | ||
func (g *Graph) Size() int { | ||
return len(g.edges) | ||
} | ||
|
||
// removeVertex removes the passed vertex as well as any edges | ||
// into the vertex. | ||
func (g *Graph) removeVertex(r object.ObjMetadata) { | ||
// First, remove the object from all adjacency lists. | ||
for v, adj := range g.edges { | ||
for i, a := range adj { | ||
if a == r { | ||
g.edges[v] = removeObj(adj, i) | ||
break | ||
} | ||
} | ||
} | ||
// Finally, remove the vertex | ||
delete(g.edges, r) | ||
} | ||
|
||
// removeObj removes the object at index "i" from the passed | ||
// list of vertices, returning the new list. | ||
func removeObj(adj []object.ObjMetadata, i int) []object.ObjMetadata { | ||
adj[len(adj)-1], adj[i] = adj[i], adj[len(adj)-1] | ||
return adj[:len(adj)-1] | ||
} | ||
|
||
// Sort returns the ordered set of vertices after | ||
// a topological sort. | ||
func (g *Graph) Sort() ([][]object.ObjMetadata, error) { | ||
sorted := [][]object.ObjMetadata{} | ||
for g.Size() > 0 { | ||
// Identify all the leaf vertices. | ||
leafVertices := []object.ObjMetadata{} | ||
for v, adj := range g.edges { | ||
if len(adj) == 0 { | ||
leafVertices = append(leafVertices, v) | ||
} | ||
} | ||
// No leaf vertices means cycle in the directed graph. | ||
if len(leafVertices) == 0 { | ||
return sorted, fmt.Errorf("cycle in directed graph") | ||
} | ||
// Remove all edges to leaf vertices. | ||
for _, v := range leafVertices { | ||
g.removeVertex(v) | ||
} | ||
sorted = append(sorted, leafVertices) | ||
} | ||
return sorted, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// Copyright 2020 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// This package provides a graph data struture | ||
// and graph functionality. | ||
package graph | ||
|
||
import ( | ||
"testing" | ||
|
||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"sigs.k8s.io/cli-utils/pkg/object" | ||
) | ||
|
||
var ( | ||
o1 = object.ObjMetadata{Name: "obj1", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} | ||
o2 = object.ObjMetadata{Name: "obj2", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} | ||
o3 = object.ObjMetadata{Name: "obj3", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} | ||
o4 = object.ObjMetadata{Name: "obj4", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} | ||
o5 = object.ObjMetadata{Name: "obj5", GroupKind: schema.GroupKind{Group: "test", Kind: "foo"}} | ||
) | ||
|
||
var ( | ||
e1 Edge = Edge{From: o1, To: o2} | ||
e2 Edge = Edge{From: o2, To: o3} | ||
e3 Edge = Edge{From: o1, To: o3} | ||
e4 Edge = Edge{From: o3, To: o4} | ||
e5 Edge = Edge{From: o2, To: o4} | ||
e6 Edge = Edge{From: o2, To: o1} | ||
e7 Edge = Edge{From: o3, To: o1} | ||
e8 Edge = Edge{From: o4, To: o5} | ||
) | ||
|
||
func TestObjectGraphSort(t *testing.T) { | ||
testCases := map[string]struct { | ||
vertices []object.ObjMetadata | ||
edges []Edge | ||
expected [][]object.ObjMetadata | ||
isError bool | ||
}{ | ||
"one edge": { | ||
vertices: []object.ObjMetadata{o1, o2}, | ||
edges: []Edge{e1}, | ||
expected: [][]object.ObjMetadata{{o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"two edges": { | ||
vertices: []object.ObjMetadata{o1, o2, o3}, | ||
edges: []Edge{e1, e2}, | ||
expected: [][]object.ObjMetadata{{o3}, {o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"three edges": { | ||
vertices: []object.ObjMetadata{o1, o2, o3}, | ||
edges: []Edge{e1, e3, e2}, | ||
expected: [][]object.ObjMetadata{{o3}, {o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"four edges": { | ||
vertices: []object.ObjMetadata{o1, o2, o3, o4}, | ||
edges: []Edge{e1, e2, e4, e5}, | ||
expected: [][]object.ObjMetadata{{o4}, {o3}, {o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"five edges": { | ||
vertices: []object.ObjMetadata{o1, o2, o3, o4}, | ||
edges: []Edge{e5, e1, e3, e2, e4}, | ||
expected: [][]object.ObjMetadata{{o4}, {o3}, {o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"no edges means all in the same first set": { | ||
vertices: []object.ObjMetadata{o1, o2, o3, o4}, | ||
edges: []Edge{}, | ||
expected: [][]object.ObjMetadata{{o4, o3, o2, o1}}, | ||
isError: false, | ||
}, | ||
"multiple objects in first set": { | ||
vertices: []object.ObjMetadata{o1, o2, o3, o4, o5}, | ||
edges: []Edge{e1, e2, e5, e8}, | ||
expected: [][]object.ObjMetadata{{o5, o3}, {o4}, {o2}, {o1}}, | ||
isError: false, | ||
}, | ||
"simple cycle in graph is an error": { | ||
vertices: []object.ObjMetadata{o1, o2}, | ||
edges: []Edge{e1, e6}, | ||
expected: [][]object.ObjMetadata{}, | ||
isError: true, | ||
}, | ||
"multi-edge cycle in graph is an error": { | ||
vertices: []object.ObjMetadata{o1, o2, o3}, | ||
edges: []Edge{e1, e2, e7}, | ||
expected: [][]object.ObjMetadata{}, | ||
isError: true, | ||
}, | ||
} | ||
|
||
for tn, tc := range testCases { | ||
t.Run(tn, func(t *testing.T) { | ||
g := New() | ||
for _, vertex := range tc.vertices { | ||
g.AddVertex(vertex) | ||
} | ||
for _, edge := range tc.edges { | ||
g.AddEdge(edge.From, edge.To) | ||
} | ||
actual, err := g.Sort() | ||
if err == nil && tc.isError { | ||
t.Fatalf("expected error, but received none") | ||
} | ||
if err != nil && !tc.isError { | ||
t.Errorf("unexpected error: %s", err) | ||
} | ||
if !tc.isError { | ||
if len(actual) != len(tc.expected) { | ||
t.Errorf("expected (%s), got (%s)", tc.expected, actual) | ||
} | ||
for i, actualSet := range actual { | ||
expectedSet := tc.expected[i] | ||
if !object.SetEquals(expectedSet, actualSet) { | ||
t.Errorf("expected sorted objects (%s), got (%s)", tc.expected, actual) | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
} |