Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

topology: Add method to retrieve all topology Objects #42

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading