internal/topology/graph_test.go
Rama main · réplica de lectura del repositorio original.
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)
}
}
}