Skip to content

Commit

Permalink
Merge pull request #42 from Kuadrant/add_topology_all
Browse files Browse the repository at this point in the history
topology: Add method to retrieve all topology Objects
  • Loading branch information
mikenairn authored Oct 17, 2024
2 parents 72b15c6 + eda338d commit db72801
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 db72801

Please sign in to comment.