Ctrl K
ComunidadGuías…

pkg/gitops/helpers_test.go

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

Archivo original ↗Preguntar a KiBot
package gitops

import (
	"fmt"
	"testing"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func TestParseFluxInventoryID(t *testing.T) {
	tests := []struct {
		name      string
		id        string
		group     string
		kind      string
		namespace string
		resource  string
		ok        bool
	}{
		{name: "deployment", id: "flux-system_podinfo_apps_Deployment", group: "apps", kind: "Deployment", namespace: "flux-system", resource: "podinfo", ok: true},
		{name: "underscores", id: "ns_my_weird_name_apps_Deployment", group: "apps", kind: "Deployment", namespace: "ns", resource: "my_weird_name", ok: true},
		{name: "core", id: "default_my-cm_core_ConfigMap", kind: "ConfigMap", namespace: "default", resource: "my-cm", ok: true},
		{name: "custom", id: "ml_train_batch.volcano.sh_Job", group: "batch.volcano.sh", kind: "Job", namespace: "ml", resource: "train", ok: true},
		{name: "cluster scoped", id: "_global_rbac.authorization.k8s.io_ClusterRole", group: "rbac.authorization.k8s.io", kind: "ClusterRole", resource: "global", ok: true},
		{name: "colon in an RBAC name", id: "_system__metrics-server_rbac.authorization.k8s.io_ClusterRole", group: "rbac.authorization.k8s.io", kind: "ClusterRole", resource: "system:metrics-server", ok: true},
		{name: "colons in a namespaced RBAC name", id: "kube-system_cert-manager__leaderelection_rbac.authorization.k8s.io_Role", group: "rbac.authorization.k8s.io", kind: "Role", namespace: "kube-system", resource: "cert-manager:leaderelection", ok: true},
		{name: "invalid", id: "only_three_parts", ok: false},
		{name: "no underscore", id: "podinfo", ok: false},
		{name: "an empty name", id: "ns__apps_Deployment", ok: false},
		{name: "an empty kind", id: "ns_podinfo_apps_", ok: false},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			group, kind, namespace, resource, ok := ParseFluxInventoryID(test.id)
			if ok != test.ok || group != test.group || kind != test.kind || namespace != test.namespace || resource != test.resource {
				t.Fatalf("ParseFluxInventoryID(%q) = (%q, %q, %q, %q, %v)", test.id, group, kind, namespace, resource, ok)
			}
		})
	}
}

// A Kustomization can list thousands of objects, and a search reads a
// manager's whole inventory to tell whether Radar can trust it.
func TestParseFluxInventoryIDAllocatesNothing(t *testing.T) {
	if n := testing.AllocsPerRun(100, func() { ParseFluxInventoryID("flux-system_my_weird_name_apps_Deployment") }); n != 0 {
		t.Fatalf("%v allocations per entry", n)
	}
}

// Telling whether Radar can read an inventory decodes no names, so an RBAC
// name Flux writes with a colon costs nothing either.
func TestFluxMembersReadableAllocatesNothing(t *testing.T) {
	entries := make([]any, 1000)
	for i := range entries {
		entries[i] = map[string]any{"id": fmt.Sprintf("kube-system_metrics-server__system__auth-delegator-%d_rbac.authorization.k8s.io_RoleBinding", i)}
	}
	ks := &unstructured.Unstructured{Object: map[string]any{"status": map[string]any{"inventory": map[string]any{"entries": entries}}}}
	if !FluxMembersReadable(ks) {
		t.Fatal("unreadable")
	}
	if n := testing.AllocsPerRun(20, func() { FluxMembersReadable(ks) }); n != 0 {
		t.Fatalf("%v allocations per check of a 1,000-entry inventory", n)
	}
}

func TestFluxTargetsLocalCluster(t *testing.T) {
	local := &unstructured.Unstructured{Object: map[string]any{"spec": map[string]any{"path": "./apps"}}}
	remote := &unstructured.Unstructured{Object: map[string]any{"spec": map[string]any{
		"kubeConfig": map[string]any{"secretRef": map[string]any{"name": "prod"}},
	}}}
	if !FluxTargetsLocalCluster(local) {
		t.Error("a Kustomization without kubeConfig applies locally")
	}
	if FluxTargetsLocalCluster(remote) {
		t.Error("a Kustomization with kubeConfig applies to another cluster")
	}
	if FluxTargetsLocalCluster(nil) {
		t.Error("nil must fail closed")
	}
}

// Shapes recorded from Argo CD 3.5.2: a live empty-render guard marks every
// resource requiresPruning; the same leftover condition after the render
// recovered and the Deployment drifted carries no requiresPruning at all.
func TestArgoCreateNamespaceEnabled(t *testing.T) {
	appWithOptions := func(opts ...any) *unstructured.Unstructured {
		return &unstructured.Unstructured{Object: map[string]any{
			"spec": map[string]any{"syncPolicy": map[string]any{"syncOptions": opts}},
		}}
	}
	tests := []struct {
		name string
		app  *unstructured.Unstructured
		want bool
	}{
		{name: "set", app: appWithOptions("CreateNamespace=true"), want: true},
		{name: "set among others", app: appWithOptions("ServerSideApply=true", "CreateNamespace=true"), want: true},
		{name: "explicitly disabled", app: appWithOptions("CreateNamespace=false"), want: false},
		{name: "other options only", app: appWithOptions("PruneLast=true"), want: false},
		// Argo matches the exact literal; a mistyped value is not the option, so
		// Radar must not treat it as set and hide the misconfiguration.
		{name: "wrong case is not the option", app: appWithOptions("CreateNamespace=TRUE"), want: false},
		{name: "empty options", app: appWithOptions(), want: false},
		{name: "no syncPolicy", app: &unstructured.Unstructured{Object: map[string]any{"spec": map[string]any{}}}, want: false},
		{name: "nil app", app: nil, want: false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := ArgoCreateNamespaceEnabled(tc.app); got != tc.want {
				t.Errorf("ArgoCreateNamespaceEnabled() = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestArgoEmptyRenderPredicates(t *testing.T) {
	app := func(sync string, resources ...any) *unstructured.Unstructured {
		status := map[string]any{"sync": map[string]any{"status": sync}}
		if resources != nil {
			status["resources"] = resources
		}
		// Argo raises the guard only for auto-sync with prune and without allowEmpty.
		spec := map[string]any{"syncPolicy": map[string]any{"automated": map[string]any{"prune": true, "selfHeal": true}}}
		return &unstructured.Unstructured{Object: map[string]any{"spec": spec, "status": status}}
	}
	withPolicy := func(u *unstructured.Unstructured, automated map[string]any) *unstructured.Unstructured {
		if automated == nil {
			unstructured.RemoveNestedField(u.Object, "spec", "syncPolicy", "automated")
		} else {
			_ = unstructured.SetNestedMap(u.Object, automated, "spec", "syncPolicy", "automated")
		}
		return u
	}
	res := func(kind string, prune any) map[string]any {
		r := map[string]any{"kind": kind, "name": "guestbook-ui", "status": "OutOfSync"}
		if prune != nil {
			r["requiresPruning"] = prune
		}
		return r
	}
	for _, tc := range []struct {
		name               string
		app                *unstructured.Unstructured
		prunes, guardHolds bool
	}{
		{"real guard", app("OutOfSync", res("Service", true), res("Deployment", true)), true, true},
		{"stale guard plus drift", app("OutOfSync", res("Service", nil), res("Deployment", nil)), false, false},
		{"one resource still rendered", app("OutOfSync", res("Service", true), res("Deployment", false)), false, false},
		{"no resources", app("OutOfSync"), false, false},
		// IgnoreExtraneous keeps such an app Synced; auto-sync never runs, but
		// a manual sync with pruning still deletes everything.
		{"synced, everything extraneous", app("Synced", res("Service", true)), true, false},
		{"nil", nil, false, false},
		// A deny window keeps the old condition after the policy changes; the
		// guard no longer holds once auto-sync can't prune everything.
		{"allowEmpty set", withPolicy(app("OutOfSync", res("Service", true)), map[string]any{"prune": true, "allowEmpty": true}), true, false},
		{"prune off", withPolicy(app("OutOfSync", res("Service", true)), map[string]any{"selfHeal": true}), true, false},
		{"auto-sync disabled in place", withPolicy(app("OutOfSync", res("Service", true)), map[string]any{"prune": true, "enabled": false}), true, false},
		{"manual", withPolicy(app("OutOfSync", res("Service", true)), nil), true, false},
	} {
		if got := ArgoSyncPrunesEverything(tc.app); got != tc.prunes {
			t.Errorf("%s: ArgoSyncPrunesEverything = %v, want %v", tc.name, got, tc.prunes)
		}
		if got := ArgoEmptyRenderGuardHolds(tc.app); got != tc.guardHolds {
			t.Errorf("%s: ArgoEmptyRenderGuardHolds = %v, want %v", tc.name, got, tc.guardHolds)
		}
	}
}

func TestToolForDecidesByAPIGroup(t *testing.T) {
	cases := []struct {
		group, kind, want string
	}{
		{"argoproj.io", "Application", ToolArgoCD},
		{"argoproj.io", "ApplicationSet", ToolArgoCD},
		{"argoproj.io", "AppProject", ToolArgoCD},
		{"kustomize.toolkit.fluxcd.io", "Kustomization", ToolFluxCD},
		{"helm.toolkit.fluxcd.io", "HelmRelease", ToolFluxCD},
		{"source.toolkit.fluxcd.io", "GitRepository", ToolFluxCD},
		// SIG Apps defines its own Application kind.
		{"app.k8s.io", "Application", ""},
		{"some.vendor", "FooApplication", ""},
		// argoproj.io is shared with Argo Rollouts and Workflows.
		{"argoproj.io", "Rollout", ""},
		{"argoproj.io", "Workflow", ""},
		{"", "Application", ""},
		{"fluxcd.io.example.com", "Kustomization", ""},
		{"fake.kustomize.toolkit.fluxcd.io", "Kustomization", ""},
		{"notargoproj.io", "Application", ""},
		{"source.toolkit.fluxcd.io", "Application", ""},
		{"source.toolkit.fluxcd.io", "Kustomization", ""},
		{"kustomize.toolkit.fluxcd.io", "HelmRelease", ""},
		{"source.toolkit.fluxcd.io", "HelmChart", ToolFluxCD},
		{"source.toolkit.fluxcd.io", "Bucket", ToolFluxCD},
		{"notification.toolkit.fluxcd.io", "Alert", ToolFluxCD},
		{"image.toolkit.fluxcd.io", "ImageRepository", ""},
	}
	for _, tc := range cases {
		if got := ToolFor(tc.group, tc.kind); got != tc.want {
			t.Errorf("ToolFor(%q, %q) = %q, want %q", tc.group, tc.kind, got, tc.want)
		}
	}
}