Ctrl K
ComunidadGuías…

pkg/k8score/apply_warnings_test.go

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

Archivo original ↗Preguntar a KiBot
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)
	}
}