Ctrl K
ComunidadGuías…

internal/topology/graph_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package topology

import (
	"context"
	"errors"
	"slices"
	"strings"
	"testing"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/dynamic/fake"
	k8stesting "k8s.io/client-go/testing"

	"github.com/sophotechlabs/spinoza/internal/api"
)

func build(t *testing.T, req Request) api.Graph {
	t.Helper()
	return Build(context.Background(), listerFor(newClient()), descs(), req)
}

func nodesByID(graph api.Graph) map[string]api.GraphNode {
	out := map[string]api.GraphNode{}
	for _, node := range graph.Nodes {
		out[node.ID] = node
	}
	return out
}

func edgeSet(graph api.Graph) map[string]bool {
	out := map[string]bool{}
	for _, edge := range graph.Edges {
		out[edge.From+"|"+edge.To+"|"+edge.Kind] = true
	}
	return out
}

func TestTheFoldPutsAWholeDeploymentOnOneNode(t *testing.T) {
	graph := build(t, Request{})
	nodes := nodesByID(graph)

	folded, ok := nodes["dep-api"]
	if !ok {
		t.Fatalf("the Deployment is missing from %v", nodes)
	}
	if folded.Contains != 3 {
		t.Fatalf("the Deployment folded %d objects, want 3 (one ReplicaSet, two pods)", folded.Contains)
	}
	if folded.Unhealthy != 1 {
		t.Fatalf("the Deployment reports %d unhealthy inside, want 1", folded.Unhealthy)
	}
	if folded.Ready != readyFalse {
		t.Fatalf("the Deployment reads %q, want False", folded.Ready)
	}
	for _, hidden := range []string{"rs-api", "pod-1", "pod-2"} {
		if _, shown := nodes[hidden]; shown {
			t.Fatalf("%q is still its own node", hidden)
		}
	}
}

func TestACompletedJobPodIsNotABrokenOne(t *testing.T) {
	nodes := nodesByID(build(t, Request{}))

	nightly, ok := nodes["cj-nightly"]
	if !ok {
		t.Fatal("the CronJob is missing")
	}
	if nightly.Contains != 2 {
		t.Fatalf("the CronJob folded %d objects, want 2 (one Job, one pod)", nightly.Contains)
	}
	if nightly.Unhealthy != 0 {
		t.Fatalf("the CronJob reports %d unhealthy inside, want 0", nightly.Unhealthy)
	}
}

func TestAnObjectWithNoUIDIsNotDrawn(t *testing.T) {
	for _, node := range build(t, Request{}).Nodes {
		if node.Name == "no-uid" {
			t.Fatal("an object with no uid became a node; the whole graph is keyed on uid")
		}
	}
}

func TestAnOwnerReferenceWithNoUIDOwnsNothing(t *testing.T) {
	graph := build(t, Request{})
	nodes := nodesByID(graph)

	orphan, ok := nodes["pod-10"]
	if !ok {
		t.Fatal("a pod whose owner reference carries no uid was dropped instead of drawn on its own")
	}
	if orphan.Contains != 0 {
		t.Fatalf("the pod folded %d objects", orphan.Contains)
	}
	for key := range edgeSet(graph) {
		if strings.HasSuffix(key, "|pod-10|owns") {
			t.Fatalf("an owns edge was drawn from an owner nothing identifies: %q", key)
		}
	}
}

func TestAnAutoscalerThatNamesNoTargetScalesNothing(t *testing.T) {
	for key := range edgeSet(build(t, Request{})) {
		if strings.HasPrefix(key, "hpa-aimless|") {
			t.Fatalf("an autoscaler with no target name grew an edge: %q", key)
		}
	}
}

func TestAContainerThatIsNotAMapConfiguresNothing(t *testing.T) {
	graph := build(t, Request{})

	if _, ok := nodesByID(graph)["job-3"]; !ok {
		t.Fatal("a Job whose container list holds junk was dropped instead of drawn")
	}
	for key := range edgeSet(graph) {
		if strings.HasSuffix(key, "|job-3|configures") {
			t.Fatalf("junk in a container list produced a config edge: %q", key)
		}
	}
}

func TestAPodItsOwnerDoesNotControlStaysOnTheGraph(t *testing.T) {
	graph := build(t, Request{})
	nodes := nodesByID(graph)

	if _, ok := nodes["pod-9"]; !ok {
		t.Fatal("a pod folded into an owner that does not control it")
	}
	if !edgeSet(graph)["dep-api|pod-9|owns"] {
		t.Fatal("the owns edge did not move to the node its folded owner sits in")
	}
}

func TestAPodNobodyOwnsStaysOnTheGraph(t *testing.T) {
	nodes := nodesByID(build(t, Request{}))

	debug, ok := nodes["pod-4"]
	if !ok {
		t.Fatal("the bare pod is missing")
	}
	if debug.Contains != 0 {
		t.Fatalf("the bare pod folded %d objects, want 0", debug.Contains)
	}
}

func TestAnOwnerNobodyListedStillGetsANode(t *testing.T) {
	nodes := nodesByID(build(t, Request{}))

	rollout, ok := nodes["rollout-1"]
	if !ok {
		t.Fatal("the Rollout is missing")
	}
	if rollout.Kind != "Rollout" {
		t.Fatalf("the owner is a %q, want Rollout", rollout.Kind)
	}
	if rollout.Resource != "rollouts" {
		t.Fatalf("the owner resolved to resource %q, want rollouts", rollout.Resource)
	}
	if rollout.Contains != 1 {
		t.Fatalf("the Rollout folded %d pods, want 1", rollout.Contains)
	}
}

func TestAClusterScopedOwnerKeepsNoNamespace(t *testing.T) {
	nodes := nodesByID(build(t, Request{}))

	fleet, ok := nodes["fleet-west"]
	if !ok {
		t.Fatal("the cluster-scoped owner is missing")
	}
	if fleet.Namespace != "" {
		t.Fatalf("the cluster-scoped owner landed in namespace %q", fleet.Namespace)
	}
}

func TestAnExpandRequestIgnoresAnEmptyID(t *testing.T) {
	nodes := nodesByID(build(t, Request{Expanded: []string{"", "dep-api"}}))

	if _, ok := nodes["rs-api"]; !ok {
		t.Fatal("an empty entry in the expand list swallowed the real one")
	}
}

func TestAServicePointsAtTheWorkloadItsPodsFoldInto(t *testing.T) {
	edges := edgeSet(build(t, Request{}))

	if !edges["svc-api|dep-api|routes"] {
		t.Fatalf("no routes edge from the Service to the Deployment: %v", edges)
	}
	if edges["svc-api|pod-1|routes"] {
		t.Fatal("the Service still points at a folded pod")
	}
}

func TestAServiceOnlyReachesPodsCarryingEveryPairItNames(t *testing.T) {
	graph := build(t, Request{})
	edges := edgeSet(graph)

	if !edges["svc-api|dep-api|routes"] {
		t.Fatal("the Service does not reach the workload whose pods carry its label")
	}
	for key := range edges {
		if strings.HasPrefix(key, "svc-api|cj-nightly") {
			t.Fatalf("the Service reached a workload whose pods carry no matching label: %q", key)
		}
		if strings.HasPrefix(key, "svc-api|pod-4") {
			t.Fatalf("the Service reached an unlabeled bare pod: %q", key)
		}
	}
}

func TestAServiceNamingALabelNoPodCarriesReachesNothing(t *testing.T) {
	for key := range edgeSet(build(t, Request{})) {
		if strings.HasPrefix(key, "svc-nomatch|") {
			t.Fatalf("a Service whose selector matches no pod grew an edge: %q", key)
		}
	}
}

func TestAServiceWithNoSelectorRoutesNowhere(t *testing.T) {
	for key := range edgeSet(build(t, Request{})) {
		if strings.HasPrefix(key, "svc-external|") {
			t.Fatalf("the ExternalName Service grew an edge: %q", key)
		}
	}
}

func TestAnIngressNamesTheServiceThatIsNotThere(t *testing.T) {
	graph := build(t, Request{})
	edges := edgeSet(graph)
	nodes := nodesByID(graph)

	if !edges["ing-web|svc-api|routes"] {
		t.Fatal("the Ingress does not reach the Service it names")
	}
	missing := "/Service/prod/gone"
	if !edges["ing-web|"+missing+"|routes"] {
		t.Fatal("the Ingress backend that is not there was dropped")
	}
	if nodes[missing].Ready != readyFalse {
		t.Fatalf("the missing Service reads %q, want False", nodes[missing].Ready)
	}
	if nodes[missing].Status != statusMissing {
		t.Fatalf("the missing Service says %q, want %q", nodes[missing].Status, statusMissing)
	}
}

func TestTheAutoscalerPointsAtWhatItScales(t *testing.T) {
	if !edgeSet(build(t, Request{}))["hpa-api|dep-api|scales"] {
		t.Fatal("no scales edge from the HorizontalPodAutoscaler to its target")
	}
}

func TestTheConfigAWorkloadMountsBecomesAnEdge(t *testing.T) {
	edges := edgeSet(build(t, Request{}))

	for _, want := range []string{
		"/ConfigMap/prod/api-config|dep-api|configures",
		"/Secret/prod/api-tls|dep-api|configures",
		"/Secret/prod/registry|dep-api|configures",
	} {
		if !edges[want] {
			t.Fatalf("missing edge %q", want)
		}
	}
}

func TestTheVolumeTheKubeletInjectsIsNotAnEdge(t *testing.T) {
	graph := build(t, Request{})
	nodes := nodesByID(graph)

	if _, drawn := nodes["/ConfigMap/prod/kube-root-ca.crt"]; drawn {
		t.Fatal("the ConfigMap from the injected token volume became a node")
	}
	for _, want := range []string{
		"/ConfigMap/prod/agent-config|ds-agent|configures",
		"/Secret/prod/agent-token|ds-agent|configures",
	} {
		if !edgeSet(graph)[want] {
			t.Fatalf("missing edge %q", want)
		}
	}
}

func TestExpandingADeploymentShowsTheReplicaSet(t *testing.T) {
	graph := build(t, Request{Expanded: []string{"dep-api"}})
	nodes := nodesByID(graph)

	replicas, ok := nodes["rs-api"]
	if !ok {
		t.Fatal("the ReplicaSet is still folded after its Deployment was expanded")
	}
	if replicas.Contains != 2 {
		t.Fatalf("the ReplicaSet folded %d pods, want 2", replicas.Contains)
	}
	if nodes["dep-api"].Contains != 0 {
		t.Fatalf("the expanded Deployment still claims %d hidden objects", nodes["dep-api"].Contains)
	}
	if !edgeSet(graph)["dep-api|rs-api|owns"] {
		t.Fatal("the owns edge between the Deployment and its ReplicaSet is missing")
	}
}

func TestExpandingTwiceReachesThePods(t *testing.T) {
	nodes := nodesByID(build(t, Request{Expanded: []string{"dep-api", "rs-api"}}))

	for _, id := range []string{"pod-1", "pod-2"} {
		if _, ok := nodes[id]; !ok {
			t.Fatalf("pod %q is still hidden with both owners expanded", id)
		}
	}
}

func TestANamespaceScopeLeavesTheRestOut(t *testing.T) {
	nodes := nodesByID(build(t, Request{Namespace: "other"}))

	if _, ok := nodes["dep-web"]; !ok {
		t.Fatal("the Deployment in the chosen namespace is missing")
	}
	if _, ok := nodes["dep-api"]; ok {
		t.Fatal("a Deployment from another namespace crossed the scope")
	}
}

func TestARootShowsWhatItTouches(t *testing.T) {
	graph := build(t, Request{Root: api.ObjectRef{
		Group:     "apps",
		Version:   "v1",
		Resource:  "deployments",
		Namespace: "prod",
		Name:      "api",
	}})
	nodes := nodesByID(graph)

	for _, want := range []string{"dep-api", "svc-api", "hpa-api", "/ConfigMap/prod/api-config", "ing-web"} {
		if _, ok := nodes[want]; !ok {
			t.Fatalf("%q is not in the neighborhood: %v", want, nodes)
		}
	}
	if _, ok := nodes["cj-nightly"]; ok {
		t.Fatal("the CronJob is unrelated to the root")
	}
	if _, ok := nodes["dep-web"]; ok {
		t.Fatal("a Deployment in another namespace reached the neighborhood")
	}
}

func TestARootNothingMatchesDrawsNothing(t *testing.T) {
	graph := build(t, Request{Root: api.ObjectRef{
		Group:     "apps",
		Version:   "v1",
		Resource:  "deployments",
		Namespace: "prod",
		Name:      "absent",
	}})
	if len(graph.Nodes) != 0 {
		t.Fatalf("a root that is not in the cluster drew %d nodes", len(graph.Nodes))
	}
}

func TestARootOfAnUnknownKindDrawsNothing(t *testing.T) {
	graph := build(t, Request{Root: api.ObjectRef{
		Group:     "apps",
		Version:   "v1",
		Resource:  "widgets",
		Namespace: "prod",
		Name:      "api",
	}})
	if len(graph.Nodes) != 0 {
		t.Fatalf("a root of a kind discovery never reported drew %d nodes", len(graph.Nodes))
	}
}

func TestARootThatIsFoldedResolvesToWhatSwallowedIt(t *testing.T) {
	graph := build(t, Request{Root: api.ObjectRef{
		Group:     "apps",
		Version:   "v1",
		Resource:  "replicasets",
		Namespace: "prod",
		Name:      "api-abc",
	}})
	if _, ok := nodesByID(graph)["dep-api"]; !ok {
		t.Fatal("a folded ReplicaSet root did not land on its Deployment")
	}
}

func TestAResourceTypeThatWillNotListIsReported(t *testing.T) {
	graph := build(t, Request{})

	want := "1 of 11 resource types could not be listed: statefulsets.apps (list statefulsets failed)"
	if graph.Error != want {
		t.Fatalf("error = %q, want %q", graph.Error, want)
	}
	if len(graph.Nodes) == 0 {
		t.Fatal("one resource type failing emptied the whole graph")
	}
}

func TestPodsBeingForbiddenLeavesTheWorkloadsStanding(t *testing.T) {
	dyn := newClient()
	dyn.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("pods is forbidden")
	})
	graph := Build(context.Background(), listerFor(dyn), descs(), Request{})
	nodes := nodesByID(graph)

	if !strings.Contains(graph.Error, "pods (pods is forbidden)") {
		t.Fatalf("the refused list is not reported: %q", graph.Error)
	}
	folded, ok := nodes["dep-api"]
	if !ok {
		t.Fatal("the Deployment went missing when its pods could not be read")
	}
	if folded.Contains != 1 {
		t.Fatalf("the Deployment folded %d objects, want just the ReplicaSet it can still see", folded.Contains)
	}
	if _, drawn := nodes["svc-api"]; !drawn {
		t.Fatal("the Service went missing when pods could not be read")
	}
	for key := range edgeSet(graph) {
		if strings.HasPrefix(key, "svc-api|") {
			t.Fatalf("a Service routed somewhere with no pods to match: %q", key)
		}
	}
}

func TestTheOutputIsSorted(t *testing.T) {
	graph := build(t, Request{})

	for i := 1; i < len(graph.Nodes); i++ {
		if graph.Nodes[i-1].ID > graph.Nodes[i].ID {
			t.Fatalf("nodes not sorted at %d: %q > %q", i, graph.Nodes[i-1].ID, graph.Nodes[i].ID)
		}
	}
	for i := 1; i < len(graph.Edges); i++ {
		previous := graph.Edges[i-1]
		current := graph.Edges[i]
		if previous.From > current.From {
			t.Fatalf("edges not sorted at %d: %q > %q", i, previous.From, current.From)
		}
		if previous.From == current.From && previous.To > current.To {
			t.Fatalf("edges not sorted at %d: %q > %q", i, previous.To, current.To)
		}
	}
}

func TestTheBuilderWarmsExactlyWhatItLists(t *testing.T) {
	spy := &warmSpy{dynLister: listerFor(newClient())}

	Build(context.Background(), spy, descs(), Request{})

	want := map[string]bool{
		"/v1/pods":                                true,
		"/v1/services":                            true,
		"/v1/replicationcontrollers":              true,
		"apps/v1/deployments":                     true,
		"apps/v1/replicasets":                     true,
		"apps/v1/statefulsets":                    true,
		"apps/v1/daemonsets":                      true,
		"batch/v1/jobs":                           true,
		"batch/v1/cronjobs":                       true,
		"networking.k8s.io/v1/ingresses":          true,
		"autoscaling/v2/horizontalpodautoscalers": true,
	}
	if len(spy.warmed) != len(want) {
		t.Fatalf("warmed %v, want the %d types the view reads", spy.warmed, len(want))
	}
	for _, key := range spy.warmed {
		if !want[key] {
			t.Fatalf("warmed %q, which this view never reads: every warm pins a cluster-wide informer", key)
		}
	}
	for key := range want {
		if !slices.Contains(spy.warmed, key) {
			t.Fatalf("%q was never warmed, so its first read waits on a cold cache", key)
		}
	}
}

func TestConfigMapsAndSecretsAreNeverListed(t *testing.T) {
	spy := &warmSpy{dynLister: listerFor(newClient())}

	Build(context.Background(), spy, descs(), Request{})

	for _, key := range spy.warmed {
		if strings.HasSuffix(key, "/configmaps") || strings.HasSuffix(key, "/secrets") {
			t.Fatalf("%q was warmed; opening this view would hold every secret in the cluster in memory", key)
		}
	}
}

func labeled(name, uid string, labels map[string]any) *unstructured.Unstructured {
	holder := meta(name, "prod", uid)
	holder["labels"] = labels
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "v1",
		"kind":       "Pod",
		"metadata":   holder,
		"status": map[string]any{
			"phase":      "Running",
			"conditions": []any{map[string]any{"type": "Ready", "status": "True"}},
		},
	}}
}

func selecting(pairs map[string]any) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "v1",
		"kind":       "Service",
		"metadata":   meta("picky", "prod", "svc-picky"),
		"spec":       map[string]any{"selector": pairs},
	}}
}

func TestASelectorNarrowsOnItsRarestLabelThenChecksTheRest(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(
		runtime.NewScheme(),
		listKinds(),
		selecting(map[string]any{"app": "api", "role": "primary"}),
		labeled("both", "pod-both", map[string]any{"app": "api", "role": "primary"}),
		labeled("role-only", "pod-role", map[string]any{"role": "primary"}),
		labeled("app-only-a", "pod-app-a", map[string]any{"app": "api"}),
		labeled("app-only-b", "pod-app-b", map[string]any{"app": "api"}),
	)

	graph := Build(context.Background(), listerFor(dyn), descs(), Request{})
	edges := edgeSet(graph)

	if !edges["svc-picky|pod-both|routes"] {
		t.Fatal("the Service does not reach the one pod carrying every pair it names")
	}
	for _, missing := range []string{"pod-role", "pod-app-a", "pod-app-b"} {
		if edges["svc-picky|"+missing+"|routes"] {
			t.Fatalf("the Service reached %s, which carries only part of its selector", missing)
		}
	}
}