package k8score import ( "context" "strings" "testing" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func ctr(name string, presentFields ...string) map[string]any { c := map[string]any{"name": name} for _, f := range presentFields { c[f] = map[string]any{} } return c } // mkWorkload builds an unstructured workload of the given kind with podSpec // placed at the kind's PodSpec path, plus optional field-manager names in // metadata.managedFields. func mkWorkload(t *testing.T, kind string, podSpec map[string]any, managers ...string) *unstructured.Unstructured { t.Helper() obj := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "apps/v1", "kind": kind, "metadata": map[string]any{"name": "w", "namespace": "ns"}, }} if podSpec != nil { if err := unstructured.SetNestedMap(obj.Object, podSpec, podSpecPath(kind)...); err != nil { t.Fatalf("set podSpec for %s: %v", kind, err) } } if len(managers) > 0 { mf := make([]any, 0, len(managers)) for _, m := range managers { mf = append(mf, map[string]any{"manager": m}) } if err := unstructured.SetNestedSlice(obj.Object, mf, "metadata", "managedFields"); err != nil { t.Fatalf("set managedFields: %v", err) } } return obj } func TestDetectExternalManager(t *testing.T) { mk := func(labels, annotations map[string]any) *unstructured.Unstructured { md := map[string]any{"name": "x"} if labels != nil { md["labels"] = labels } if annotations != nil { md["annotations"] = annotations } return &unstructured.Unstructured{Object: map[string]any{"kind": "Deployment", "metadata": md}} } tests := []struct { name string obj *unstructured.Unstructured wantManager string wantInstance string }{ { // Argo apps frequently carry Helm annotations because Argo renders // charts — Argo must win the precedence ladder. name: "argo beats helm", obj: mk(map[string]any{"argocd.argoproj.io/instance": "my-app"}, map[string]any{"meta.helm.sh/release-name": "rel"}), wantManager: "Argo CD", wantInstance: "my-app", }, { name: "argo tracking-id only", obj: mk(nil, map[string]any{"argocd.argoproj.io/tracking-id": "track-1"}), wantManager: "Argo CD", wantInstance: "track-1", }, { name: "flux kustomization namespace-qualified", obj: mk(map[string]any{"kustomize.toolkit.fluxcd.io/name": "ks", "kustomize.toolkit.fluxcd.io/namespace": "flux-system"}, nil), wantManager: "Flux (Kustomization)", wantInstance: "flux-system/ks", }, { name: "flux kustomization bare name", obj: mk(map[string]any{"kustomize.toolkit.fluxcd.io/name": "ks"}, nil), wantManager: "Flux (Kustomization)", wantInstance: "ks", }, { name: "flux helmrelease namespace-qualified", obj: mk(map[string]any{"helm.toolkit.fluxcd.io/name": "hr", "helm.toolkit.fluxcd.io/namespace": "apps"}, nil), wantManager: "Flux (HelmRelease)", wantInstance: "apps/hr", }, { name: "flux kustomization beats helm label", obj: mk(map[string]any{"app.kubernetes.io/managed-by": "Helm", "kustomize.toolkit.fluxcd.io/name": "ks"}, nil), wantManager: "Flux (Kustomization)", wantInstance: "ks", }, { // Argo CD and Flux write these as labels; an annotation of the same // name is not their tracking. name: "flux and argo keys as annotations are ignored", obj: mk(nil, map[string]any{"kustomize.toolkit.fluxcd.io/name": "ks", "argocd.argoproj.io/instance": "my-app"}), wantManager: "", }, { name: "helm release annotation", obj: mk(nil, map[string]any{"meta.helm.sh/release-name": "rel"}), wantManager: "Helm", wantInstance: "rel", }, { name: "helm label only uses instance label", obj: mk(map[string]any{"app.kubernetes.io/managed-by": "Helm", "app.kubernetes.io/instance": "inst"}, nil), wantManager: "Helm", wantInstance: "inst", }, { name: "kubectl excluded", obj: mk(map[string]any{"app.kubernetes.io/managed-by": "kubectl"}, nil), wantManager: "", }, { // Case-insensitive exclusion — a regression making this case-sensitive // would warn on every radar-applied resource. name: "radar excluded case-insensitive", obj: mk(map[string]any{"app.kubernetes.io/managed-by": "Radar"}, nil), wantManager: "", }, { name: "generic operator", obj: mk(map[string]any{"app.kubernetes.io/managed-by": "my-operator", "app.kubernetes.io/instance": "inst"}, nil), wantManager: "my-operator", wantInstance: "inst", }, { name: "no metadata", obj: mk(nil, nil), wantManager: "", }, { name: "nil object", obj: nil, wantManager: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mgr, inst := detectExternalManager(tt.obj) if mgr != tt.wantManager { t.Errorf("manager = %q, want %q", mgr, tt.wantManager) } if tt.wantManager != "" && inst != tt.wantInstance { t.Errorf("instance = %q, want %q", inst, tt.wantInstance) } }) } } // Metadata names a candidate, not a verdict: the warning must not state that the // manager reconciles the object, and native Helm only acts on upgrade. func TestExternalManagerWarningReportsMetadata(t *testing.T) { argo := formatExternalManagerWarning("Argo CD", "shop") if strings.Contains(argo, "is reconciled by") || !strings.Contains(argo, "tracking metadata names Argo CD (instance: shop)") { t.Errorf("Argo CD warning = %q", argo) } helm := formatExternalManagerWarning("Helm", "web") if strings.Contains(helm, "reconcil") || !strings.Contains(helm, "helm upgrade") { t.Errorf("Helm warning = %q", helm) } } func TestCheckFieldRemoval(t *testing.T) { t.Run("deployment container probe survives", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}, "kube-controller-manager", "radar") w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.containers[name=app].readinessProbe") { t.Errorf("fieldRef wrong: %s", w[0]) } if !strings.Contains(w[0], "kube-controller-manager") { t.Errorf("expected other-manager attribution: %s", w[0]) } }) t.Run("cronjob field path", func(t *testing.T) { pre := mkWorkload(t, "CronJob", map[string]any{"containers": []any{ctr("app", "livenessProbe")}}) subm := mkWorkload(t, "CronJob", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "CronJob", map[string]any{"containers": []any{ctr("app", "livenessProbe")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.jobTemplate.spec.template.spec.containers[name=app].livenessProbe") { t.Errorf("cronjob fieldRef wrong: %s", w[0]) } }) t.Run("pod field path", func(t *testing.T) { pre := mkWorkload(t, "Pod", map[string]any{"containers": []any{ctr("app", "resources")}}) subm := mkWorkload(t, "Pod", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Pod", map[string]any{"containers": []any{ctr("app", "resources")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.containers[name=app].resources") { t.Errorf("pod fieldRef wrong: %s", w[0]) } }) t.Run("pod-level nodeSelector survives", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"nodeSelector": map[string]any{"disktype": "ssd"}, "containers": []any{ctr("app")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"nodeSelector": map[string]any{"disktype": "ssd"}, "containers": []any{ctr("app")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.nodeSelector") { t.Errorf("nodeSelector fieldRef wrong: %s", w[0]) } // No other manager → the plain "set it to null" remediation. if !strings.Contains(w[0], "set its value to null") { t.Errorf("expected null-removal remediation: %s", w[0]) } }) t.Run("field still in submission is not flagged", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}) if w := checkFieldRemoval(subm, pre, post); len(w) != 0 { t.Fatalf("want no warning when field kept, got %v", w) } }) t.Run("successful removal is not flagged", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "readinessProbe")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) if w := checkFieldRemoval(subm, pre, post); len(w) != 0 { t.Fatalf("want no warning when field actually removed, got %v", w) } }) t.Run("matches container by name not index", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app", "resources"), ctr("side")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("side"), ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("side"), ctr("app", "resources")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "name=app") || strings.Contains(w[0], "name=side") { t.Errorf("warning should key on app, not side: %s", w[0]) } }) t.Run("omitted sidecar survives", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app"), ctr("sidecar")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app"), ctr("sidecar")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.containers[name=sidecar]") { t.Errorf("sidecar fieldRef wrong: %s", w[0]) } }) t.Run("pod dns policy survives", func(t *testing.T) { pre := mkWorkload(t, "Deployment", map[string]any{"dnsPolicy": "None", "containers": []any{ctr("app")}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"dnsPolicy": "None", "containers": []any{ctr("app")}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.dnsPolicy") { t.Errorf("dnsPolicy fieldRef wrong: %s", w[0]) } }) t.Run("container env survives", func(t *testing.T) { withEnv := func() map[string]any { return map[string]any{"name": "app", "env": []any{map[string]any{"name": "API_URL", "value": "api:8080"}}} } pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{withEnv()}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{withEnv()}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.containers[name=app].env") { t.Errorf("env fieldRef wrong: %s", w[0]) } }) t.Run("container hostPort survives", func(t *testing.T) { withHostPort := func() map[string]any { return map[string]any{"name": "app", "ports": []any{map[string]any{"containerPort": int64(8080), "hostPort": int64(18080)}}} } pre := mkWorkload(t, "Deployment", map[string]any{"containers": []any{withHostPort()}}) subm := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) post := mkWorkload(t, "Deployment", map[string]any{"containers": []any{withHostPort()}}) w := checkFieldRemoval(subm, pre, post) if len(w) != 1 { t.Fatalf("want 1 warning, got %d: %v", len(w), w) } if !strings.Contains(w[0], "spec.template.spec.containers[name=app].ports[containerPort=8080].hostPort") { t.Errorf("hostPort fieldRef wrong: %s", w[0]) } }) t.Run("nil inputs", func(t *testing.T) { ok := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) if w := checkFieldRemoval(nil, ok, ok); w != nil { t.Errorf("nil submitted should yield nil, got %v", w) } if w := checkFieldRemoval(ok, nil, ok); w != nil { t.Errorf("nil pre should yield nil, got %v", w) } }) } func TestPopulateApplyWarningsPostGetFailure(t *testing.T) { result := &ApplyResourceResult{} submitted := mkWorkload(t, "Deployment", map[string]any{"containers": []any{ctr("app")}}) pre := submitted.DeepCopy() m := &WorkloadManager{} m.populateApplyWarnings(context.Background(), result, submitted, pre, nil, "ns", "Deployment", "w", false) if len(result.Warnings) != 1 || !strings.Contains(result.Warnings[0], "Post-apply verification GET failed") { t.Fatalf("warnings = %+v, want post-apply verification warning", result.Warnings) } } func TestPodSpecReferences(t *testing.T) { tests := []struct { name string kind string podSpec map[string]any refKind string refName string want bool }{ { name: "env configMapKeyRef", kind: "Deployment", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "env": []any{map[string]any{"name": "X", "valueFrom": map[string]any{"configMapKeyRef": map[string]any{"name": "cfg"}}}}}}}, refKind: "ConfigMap", refName: "cfg", want: true, }, { name: "env secretKeyRef", kind: "Deployment", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "env": []any{map[string]any{"name": "X", "valueFrom": map[string]any{"secretKeyRef": map[string]any{"name": "sec"}}}}}}}, refKind: "Secret", refName: "sec", want: true, }, { name: "envFrom configMapRef", kind: "Deployment", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "envFrom": []any{map[string]any{"configMapRef": map[string]any{"name": "cfg"}}}}}}, refKind: "ConfigMap", refName: "cfg", want: true, }, { name: "volume configMap.name", kind: "Deployment", podSpec: map[string]any{"containers": []any{ctr("app")}, "volumes": []any{map[string]any{"name": "v", "configMap": map[string]any{"name": "cfg"}}}}, refKind: "ConfigMap", refName: "cfg", want: true, }, { // Secret volumes key on secretName, not name — a regression copying // the ConfigMap shape would silently miss every secret-volume consumer. name: "volume secret.secretName", kind: "Deployment", podSpec: map[string]any{"containers": []any{ctr("app")}, "volumes": []any{map[string]any{"name": "v", "secret": map[string]any{"secretName": "sec"}}}}, refKind: "Secret", refName: "sec", want: true, }, { name: "volume secret.name does not match", kind: "Deployment", podSpec: map[string]any{"containers": []any{ctr("app")}, "volumes": []any{map[string]any{"name": "v", "secret": map[string]any{"name": "sec"}}}}, refKind: "Secret", refName: "sec", want: false, }, { name: "projected configMap source", kind: "Deployment", podSpec: map[string]any{"containers": []any{ctr("app")}, "volumes": []any{map[string]any{"name": "v", "projected": map[string]any{"sources": []any{map[string]any{"configMap": map[string]any{"name": "cfg"}}}}}}}, refKind: "ConfigMap", refName: "cfg", want: true, }, { // Projected secret sources key on name (unlike a plain secret volume). name: "projected secret source uses name", kind: "Deployment", podSpec: map[string]any{"containers": []any{ctr("app")}, "volumes": []any{map[string]any{"name": "v", "projected": map[string]any{"sources": []any{map[string]any{"secret": map[string]any{"name": "sec"}}}}}}}, refKind: "Secret", refName: "sec", want: true, }, { name: "initContainer env match", kind: "Deployment", podSpec: map[string]any{"initContainers": []any{map[string]any{"name": "init", "envFrom": []any{map[string]any{"configMapRef": map[string]any{"name": "cfg"}}}}}, "containers": []any{ctr("app")}}, refKind: "ConfigMap", refName: "cfg", want: true, }, { name: "name mismatch", kind: "Deployment", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "envFrom": []any{map[string]any{"configMapRef": map[string]any{"name": "other"}}}}}}, refKind: "ConfigMap", refName: "cfg", want: false, }, { name: "wrong refKind", kind: "Deployment", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "envFrom": []any{map[string]any{"configMapRef": map[string]any{"name": "cfg"}}}}}}, refKind: "Secret", refName: "cfg", want: false, }, { name: "cronjob nested path", kind: "CronJob", podSpec: map[string]any{"containers": []any{map[string]any{"name": "app", "envFrom": []any{map[string]any{"configMapRef": map[string]any{"name": "cfg"}}}}}}, refKind: "ConfigMap", refName: "cfg", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { obj := mkWorkload(t, tt.kind, tt.podSpec) if got := podSpecReferences(obj, tt.refKind, tt.refName); got != tt.want { t.Errorf("podSpecReferences = %v, want %v", got, tt.want) } }) } } func TestFormatConsumerWarning(t *testing.T) { if got := formatConsumerWarning("ConfigMap", "cfg", nil, false); got != "" { t.Errorf("verified-empty should yield no warning, got %q", got) } if got := formatConsumerWarning("ConfigMap", "cfg", nil, true); !strings.Contains(got, "Could not fully enumerate") { t.Errorf("partial-empty should hedge, got %q", got) } full := formatConsumerWarning("ConfigMap", "cfg", []string{"Deployment/a", "Deployment/b"}, false) if !strings.Contains(full, "2 workload(s)") || strings.Contains(full, "incomplete") { t.Errorf("complete list should not be marked incomplete: %q", full) } partial := formatConsumerWarning("ConfigMap", "cfg", []string{"Deployment/a"}, true) if !strings.Contains(partial, "incomplete") { t.Errorf("partial list should be flagged incomplete: %q", partial) } many := make([]string, 7) for i := range many { many[i] = "Deployment/d" } if got := formatConsumerWarning("Secret", "s", many, false); !strings.Contains(got, "(+2 more)") { t.Errorf("expected truncation marker for >5 consumers: %q", got) } }