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