pkg/finalizers/catalog_test.go
Rama main · réplica de lectura del repositorio original.
package finalizers
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestOwnerMatchesWorkload(t *testing.T) {
vm := &unstructured.Unstructured{}
vm.SetAPIVersion("operator.victoriametrics.com/v1beta1")
argo := &unstructured.Unstructured{}
argo.SetAPIVersion("argoproj.io/v1alpha1")
kustomization := &unstructured.Unstructured{}
kustomization.SetAPIVersion("kustomize.toolkit.fluxcd.io/v1")
renamed := map[string]string{"app.kubernetes.io/name": "vmop"}
containers := func(image string, args ...string) corev1.PodTemplateSpec {
return corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: renamed}, Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "operator", Image: image, Args: args}}}}
}
for _, tt := range []struct {
name, finalizer, workload string
root *unstructured.Unstructured
labels map[string]string
template corev1.PodTemplateSpec
want bool
}{
{name: "AWS node class", finalizer: "karpenter.k8s.aws/termination", root: vm, workload: "karpenter", want: true},
{name: "Azure node class", finalizer: "karpenter.azure.com/termination", root: vm, workload: "karpenter", want: true},
{name: "karpenter deployment", finalizer: "karpenter.sh/termination", root: vm, workload: "karpenter", want: true},
{name: "catalog label", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "custom", labels: map[string]string{"app.kubernetes.io/name": "victoria-metrics-operator"}, want: true},
{name: "catalog name", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "vm-victoria-metrics-operator", want: true},
{name: "renamed install by Docker Hub image", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "vmop", labels: renamed, template: containers("victoriametrics/operator:v0.50.0"), want: true},
{name: "renamed install by mirrored image", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "vmop", labels: renamed, template: containers("mirror.example.com:5000/victoriametrics/operator:v0.50.0@sha256:0123"), want: true},
{name: "mirror that prefixes the path", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "vmop", labels: renamed, template: containers("mirror.example.com/dockerhub/victoriametrics/operator:v0.50.0"), want: true},
{name: "renamed install of another image", finalizer: "apps.victoriametrics.com/finalizer", root: vm, workload: "vmop", labels: renamed, template: containers("victoriametrics/vmagent:v1.100.0")},
{name: "flux controller by ghcr image", finalizer: "finalizers.kustomize.toolkit.fluxcd.io", root: kustomization, workload: "kc", template: containers("ghcr.io/fluxcd/kustomize-controller:v1.4.0"), want: true},
{name: "flux controller image of a sibling", finalizer: "finalizers.kustomize.toolkit.fluxcd.io", root: kustomization, workload: "hc", template: containers("ghcr.io/fluxcd/helm-controller:v1.1.0")},
{name: "argo cd application controller by image and binary", finalizer: "resources-finalizer.argocd.argoproj.io", root: argo, workload: "ctrl", template: containers("quay.io/argoproj/argocd:v2.13.0", "/usr/local/bin/argocd-application-controller", "--metrics-port=8082"), want: true},
{name: "argo cd server shares the image", finalizer: "resources-finalizer.argocd.argoproj.io", root: argo, workload: "web", template: containers("quay.io/argoproj/argocd:v2.13.0", "/usr/local/bin/argocd-server")},
} {
t.Run(tt.name, func(t *testing.T) {
owner := ResolveOwner(tt.finalizer, tt.root)
if owner == nil {
t.Fatal("expected a catalog owner")
}
workload := &metav1.ObjectMeta{Name: tt.workload, Labels: tt.labels}
if got := owner.MatchesWorkload(workload, tt.template); got != tt.want {
t.Fatalf("got %v want %v", got, tt.want)
}
})
}
}
func TestVictoriaMetricsReloaderImageIsNotOperator(t *testing.T) {
root := &unstructured.Unstructured{}
root.SetAPIVersion("operator.victoriametrics.com/v1beta1")
owner := ResolveOwner("apps.victoriametrics.com/finalizer", root)
for _, tc := range []struct {
name string
containers []corev1.Container
want bool
}{
{name: "default reloader", containers: []corev1.Container{{Name: "config-reloader", Image: "victoriametrics/operator:config-reloader-v0.66.1"}}},
{name: "renamed reloader container", containers: []corev1.Container{{Name: "reload", Image: "victoriametrics/operator:config-reloader-v0.66.1"}}},
{name: "mirrored tagged digest reloader", containers: []corev1.Container{{Name: "reload", Image: "mirror.example:5000/victoriametrics/operator:config-reloader-v0.66.1@sha256:0123"}}},
{name: "digest-only reloader", containers: []corev1.Container{{Name: "config-reloader", Image: "mirror.example:5000/victoriametrics/operator@sha256:0123"}}},
{name: "retagged reloader", containers: []corev1.Container{{Name: "config-reloader", Image: "victoriametrics/operator:custom"}}},
{name: "operator with default entrypoint", containers: []corev1.Container{{Name: "manager", Image: "victoriametrics/operator:v0.66.1"}}, want: true},
{name: "digest-only operator", containers: []corev1.Container{{Name: "manager", Image: "mirror.example:5000/victoriametrics/operator@sha256:0123"}}, want: true},
{name: "operator beside reloader", containers: []corev1.Container{{Name: "config-reloader", Image: "victoriametrics/operator:config-reloader-v0.66.1"}, {Name: "manager", Image: "victoriametrics/operator:v0.66.1"}}, want: true},
} {
t.Run(tc.name, func(t *testing.T) {
spec := corev1.PodSpec{Containers: tc.containers}
if got := owner.MatchesPod(nil, spec); got != tc.want {
t.Errorf("pod match = %v, want %v", got, tc.want)
}
if got := owner.MatchesWorkload(&metav1.ObjectMeta{Name: "custom"}, corev1.PodTemplateSpec{Spec: spec}); got != tc.want {
t.Errorf("workload match = %v, want %v", got, tc.want)
}
})
}
}
func TestImageRepositoryPath(t *testing.T) {
for image, want := range map[string]string{
"victoriametrics/operator": "victoriametrics/operator",
"victoriametrics/operator:v0.50.0": "victoriametrics/operator",
"docker.io/victoriametrics/operator:v0.50.0": "victoriametrics/operator",
"ghcr.io/fluxcd/helm-controller:v1.1.0@sha256:abc": "fluxcd/helm-controller",
"localhost/fluxcd/helm-controller": "fluxcd/helm-controller",
"registry:5000/fluxcd/helm-controller:v1": "fluxcd/helm-controller",
"public.ecr.aws/karpenter/controller:1.0.0": "karpenter/controller",
"mcr.microsoft.com/aks/karpenter/controller:v1.0.0": "aks/karpenter/controller",
"mirror.example.com/dockerhub/victoriametrics/operator:v1": "dockerhub/victoriametrics/operator",
} {
if got := imageRepositoryPath(image); got != want {
t.Errorf("%s: got %q want %q", image, got, want)
}
}
}
// A first-label name match is too weak to identify an owner: it may only
// name a candidate.
func TestControllerCandidate(t *testing.T) {
root := &unstructured.Unstructured{}
for _, tt := range []struct {
name, group, finalizer, workload string
labels map[string]string
want bool
}{
{name: "name", group: "widgets.example.com", finalizer: "widgets.example.com/cleanup", workload: "release-widgets-operator", want: true},
{name: "part-of", group: "widgets.example.com", finalizer: "widgets.example.com/cleanup", workload: "custom", labels: map[string]string{"app.kubernetes.io/part-of": "widgets-operator"}, want: true},
{name: "unrelated domain", group: "other.example.com", finalizer: "widgets.example.com/cleanup", workload: "widgets-operator"},
{name: "domain only", group: "widgets.example.com", finalizer: "widgets.example.com/cleanup", workload: "widgets-server"},
{name: "substring", group: "widgets.example.com", finalizer: "widgets.example.com/cleanup", workload: "notwidgets-operator"},
{name: "reserved domain", group: "k8s.io", finalizer: "k8s.io/cleanup", workload: "k8s-operator"},
{name: "catalog finalizers are never candidates", group: "karpenter.sh", finalizer: "karpenter.sh/termination", workload: "karpenter-operator"},
} {
t.Run(tt.name, func(t *testing.T) {
root.SetAPIVersion(tt.group + "/v1")
workload := &metav1.ObjectMeta{Name: tt.workload, Labels: tt.labels}
if got := ControllerCandidate(tt.finalizer, root, workload); got != tt.want {
t.Fatalf("got %v want %v", got, tt.want)
}
})
}
}
func TestResolveKarpenterProviderOwner(t *testing.T) {
for _, key := range []string{"karpenter.sh/termination", "karpenter.sh/custom", "karpenter.k8s.aws/termination", "karpenter.azure.com/termination"} {
t.Run(key, func(t *testing.T) {
owner := ResolveOwner(key, &unstructured.Unstructured{})
if owner == nil || owner.Controller != "karpenter" || owner.Namespace != "" || owner.SelectorKey != "app.kubernetes.io/name" || owner.SelectorValue != "karpenter" || owner.OffClusterOfferings == "" || !owner.ReleasesInfrastructure {
t.Fatalf("got owner %+v", owner)
}
})
}
}
func TestOwnerOffClusterAndInfrastructure(t *testing.T) {
argo := ResolveOwner("resources-finalizer.argocd.argoproj.io", &unstructured.Unstructured{})
if argo == nil || argo.OffClusterOfferings != "Amazon EKS Capabilities" || argo.ReleasesInfrastructure {
t.Fatalf("argo=%+v", argo)
}
vm := &unstructured.Unstructured{}
vm.SetAPIVersion("operator.victoriametrics.com/v1beta1")
for _, owner := range []*Owner{ResolveOwner("apps.victoriametrics.com/finalizer", vm), ResolveOwner("finalizers.helm.toolkit.fluxcd.io", vm)} {
if owner == nil || owner.OffClusterOfferings != "" || owner.ReleasesInfrastructure {
t.Fatalf("in-cluster catalog entry=%+v", owner)
}
}
}
// Infrastructure operators stay out of the catalog: absent an identified
// controller, callers must not treat a missing pod as permission to remove
// the finalizer.
func TestInfrastructureOperatorFinalizersAreNotIdentified(t *testing.T) {
for finalizer, group := range map[string]string{
"finalizer.managedresource.crossplane.io": "ec2.aws.upbound.io",
"composite.apiextensions.crossplane.io": "platform.example.org",
"cluster.cluster.x-k8s.io": "cluster.x-k8s.io",
"cnrm.cloud.google.com/finalizer": "sql.cnrm.cloud.google.com",
"finalizers.s3.services.k8s.aws": "s3.services.k8s.aws",
"strimzi.io/topic-operator": "kafka.strimzi.io",
} {
root := &unstructured.Unstructured{}
root.SetAPIVersion(group + "/v1")
if owner := ResolveOwner(finalizer, root); owner != nil {
t.Errorf("%s resolved to %+v", finalizer, owner)
}
for _, workload := range []string{"crossplane", "capi-controller-manager", "cnrm-controller-manager", "ack-s3-controller", "strimzi-cluster-operator"} {
if ControllerCandidate(finalizer, root, &metav1.ObjectMeta{Name: workload}) {
t.Errorf("%s matched workload %s", finalizer, workload)
}
}
}
}
func TestFluxImageOwnerByKind(t *testing.T) {
for kind, want := range map[string]string{"ImageUpdateAutomation": "image-automation-controller", "ImagePolicy": "image-reflector-controller", "ImageRepository": "image-reflector-controller"} {
root := &unstructured.Unstructured{}
root.SetAPIVersion("image.toolkit.fluxcd.io/v1beta2")
root.SetKind(kind)
for _, finalizer := range []string{"finalizers.fluxcd.io", "finalizers.image.toolkit.fluxcd.io"} {
if owner := ResolveOwner(finalizer, root); owner == nil || owner.Controller != want || owner.SelectorValue != want {
t.Errorf("%s %s: got %+v, want %s", kind, finalizer, owner, want)
}
}
}
}
// TestResolveOwner pins the static finalizer→controller mapping.
// New Flux/Argo versions may introduce new keys; this test catches
// regressions when entries get accidentally renamed.
func TestResolveOwner(t *testing.T) {
mkObj := func(api string) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": api,
"kind": "Whatever",
}}
}
tests := []struct {
name string
finalizer string
obj *unstructured.Unstructured
want string // expected Controller name; "" means nil owner
}{
{"argo standard", "resources-finalizer.argocd.argoproj.io", mkObj(""), "argocd-application-controller"},
{"argo legacy cascade", "foreground-cascade.argocd.argoproj.io", mkObj(""), "argocd-application-controller"},
{"flux generic on Kustomization", "finalizers.fluxcd.io", mkObj("kustomize.toolkit.fluxcd.io/v1"), "kustomize-controller"},
{"flux generic on HelmRelease", "finalizers.fluxcd.io", mkObj("helm.toolkit.fluxcd.io/v2"), "helm-controller"},
{"flux generic on GitRepository", "finalizers.fluxcd.io", mkObj("source.toolkit.fluxcd.io/v1"), "source-controller"},
{"flux specific kustomize", "finalizers.kustomize.toolkit.fluxcd.io", mkObj(""), "kustomize-controller"},
{"flux specific helm", "finalizers.helm.toolkit.fluxcd.io", mkObj(""), "helm-controller"},
{"flux specific source", "finalizers.source.toolkit.fluxcd.io", mkObj(""), "source-controller"},
{"unknown finalizer returns nil", "example.com/cleanup", mkObj(""), ""},
{"unknown apiVersion for generic flux returns nil", "finalizers.fluxcd.io", mkObj("custom.example.com/v1"), ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ResolveOwner(tt.finalizer, tt.obj)
if tt.want == "" {
if got != nil {
t.Fatalf("expected nil owner, got %+v", got)
}
return
}
if got == nil {
t.Fatalf("expected owner with controller %q, got nil", tt.want)
}
if got.Controller != tt.want {
t.Fatalf("Controller = %q, want %q", got.Controller, tt.want)
}
})
}
}
func TestCatalogConditionalCleanupPolicy(t *testing.T) {
for _, tc := range []struct {
finalizer, group, kind string
allow bool
}{
{"apps.victoriametrics.com/finalizer", "operator.victoriametrics.com", "VMAgent", true},
{"finalizers.kustomize.toolkit.fluxcd.io", "kustomize.toolkit.fluxcd.io", "Kustomization", true},
{"finalizers.helm.toolkit.fluxcd.io", "helm.toolkit.fluxcd.io", "HelmRelease", true},
{"finalizers.source.toolkit.fluxcd.io", "source.toolkit.fluxcd.io", "GitRepository", true},
{"finalizers.notification.toolkit.fluxcd.io", "notification.toolkit.fluxcd.io", "Alert", true},
{"finalizers.image.toolkit.fluxcd.io", "image.toolkit.fluxcd.io", "ImageRepository", true},
{"finalizers.image.toolkit.fluxcd.io", "image.toolkit.fluxcd.io", "ImageUpdateAutomation", true},
{"resources-finalizer.argocd.argoproj.io", "argoproj.io", "Application", false},
{"karpenter.sh/termination", "karpenter.sh", "NodeClaim", false},
{"karpenter.k8s.aws/termination", "karpenter.k8s.aws", "EC2NodeClass", false},
{"karpenter.azure.com/termination", "karpenter.azure.com", "AKSNodeClass", false},
} {
t.Run(tc.finalizer+"/"+tc.kind, func(t *testing.T) {
root := &unstructured.Unstructured{}
root.SetAPIVersion(tc.group + "/v1")
root.SetKind(tc.kind)
owner := ResolveOwner(tc.finalizer, root)
if owner == nil || owner.AllowConditionalCleanup != tc.allow {
t.Fatalf("owner=%+v, want cleanup opt-in %v", owner, tc.allow)
}
})
}
}
func TestFluxHelmControllerRequiresPublishedImage(t *testing.T) {
root := &unstructured.Unstructured{}
root.SetAPIVersion("helm.toolkit.fluxcd.io/v2")
owner := ResolveOwner("finalizers.helm.toolkit.fluxcd.io", root)
for _, tc := range []struct {
name, image string
want bool
}{
{"unrelated k3s controller", "rancher/helm-controller:v0.16.0", false},
{"unrelated private controller", "private.example/team/helm-controller:v1", false},
{"Flux controller", "ghcr.io/fluxcd/helm-controller:v1.1.0", true},
{"Flux controller mirrored", "mirror.example/fluxcd/helm-controller@sha256:abc", true},
{"Flux controller proxy cache", "harbor.example/ghcr-proxy/fluxcd/helm-controller:v1", true},
{"Flux controller prefix mirror", "registry.example/oss/fluxcd/helm-controller:v1", true},
{"vendor segment must match", "registry.example/team/notfluxcd/helm-controller:v1", false},
} {
t.Run(tc.name, func(t *testing.T) {
identity := metav1.ObjectMeta{Name: "helm-controller", Labels: map[string]string{"app": "helm-controller"}}
template := corev1.PodTemplateSpec{ObjectMeta: identity, Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "manager", Image: tc.image}}}}
if got := owner.MatchesWorkload(&identity, template); got != tc.want {
t.Fatalf("workload match = %v, want %v", got, tc.want)
}
if got := owner.MatchesPod(identity.Labels, template.Spec); got != tc.want {
t.Fatalf("pod match = %v, want %v", got, tc.want)
}
})
}
}