Skip to content

Commit

Permalink
topology: Add method to retrieve all topology Objects
Browse files Browse the repository at this point in the history
Adds an "All()" method to the topology that returns all Objects in the
topology graph i.e. Objects + Targetables + Policies.

Exposing a collection like this allows API users to more easily query
the paths between any two objects regardless of type e.g Policy ->
Object, Targetable -> Object etc..

Signed-off-by: Michael Nairn <[email protected]>
  • Loading branch information
mikenairn committed Oct 17, 2024
1 parent 72b15c6 commit eda338d
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 9 deletions.
11 changes: 11 additions & 0 deletions machinery/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 15 additions & 3 deletions machinery/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ type Topology struct {
}

// 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,
Expand All @@ -151,7 +150,6 @@ func (t *Topology) Targetables() *collection[Targetable] {
}

// 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,
Expand All @@ -160,14 +158,28 @@ func (t *Topology) Policies() *collection[Policy] {
}

// 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,
}
}

// All returns all object nodes in the topology.
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()
}
Expand Down
193 changes: 187 additions & 6 deletions machinery/topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
})
}
}
4 changes: 4 additions & 0 deletions machinery/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit eda338d

Please sign in to comment.