pkg/k8score/workload_delete_test.go
Rama main · réplica de lectura del repositorio original.
package k8score
import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
)
// TestDeleteResourceSendsBackgroundPropagation pins the delete request to
// kubectl's: an explicit background policy, so a Job's Pods go with it
// instead of being orphaned by the API server's legacy default for Jobs.
func TestDeleteResourceSendsBackgroundPropagation(t *testing.T) {
gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}
for _, force := range []bool{false, true} {
disc, err := NewResourceDiscovery(fake.NewSimpleClientset().Discovery())
if err != nil {
t.Fatal(err)
}
disc.AddAPIResource(APIResource{Group: "batch", Version: "v1", Kind: "Job", Name: "jobs", Namespaced: true, Verbs: []string{"delete"}})
job := &unstructured.Unstructured{}
job.SetAPIVersion("batch/v1")
job.SetKind("Job")
job.SetNamespace("team")
job.SetName("nightly")
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{gvr: "JobList"}, job)
var deletes []clienttesting.DeleteActionImpl
dyn.PrependReactor("delete", "jobs", func(a clienttesting.Action) (bool, runtime.Object, error) {
deletes = append(deletes, a.(clienttesting.DeleteActionImpl))
return false, nil, nil
})
err = NewWorkloadManager(dyn, disc).DeleteResource(context.Background(), DeleteResourceOptions{Kind: "Job", Group: "batch", Namespace: "team", Name: "nightly", Force: force})
if err != nil {
t.Fatalf("force=%v: %v", force, err)
}
if len(deletes) != 1 {
t.Fatalf("force=%v: %d delete requests, want 1", force, len(deletes))
}
got := deletes[0].DeleteOptions
if got.PropagationPolicy == nil || *got.PropagationPolicy != metav1.DeletePropagationBackground {
t.Errorf("force=%v: propagationPolicy = %v, want Background", force, got.PropagationPolicy)
}
if force != (got.GracePeriodSeconds != nil && *got.GracePeriodSeconds == 0) {
t.Errorf("force=%v: gracePeriodSeconds = %v", force, got.GracePeriodSeconds)
}
}
}
func TestDeleteResourcePreservesPendingDeletionPolicy(t *testing.T) {
disc, err := NewResourceDiscovery(fake.NewSimpleClientset().Discovery())
if err != nil {
t.Fatal(err)
}
disc.AddAPIResource(APIResource{Group: "batch", Version: "v1", Kind: "Job", Name: "jobs", Namespaced: true, Verbs: []string{"delete"}})
for _, tt := range []struct {
name string
finalizer string
terminating bool
wantBackground bool
}{
{name: "terminating foreground", finalizer: metav1.FinalizerDeleteDependents, terminating: true},
{name: "terminating orphan", finalizer: metav1.FinalizerOrphanDependents, terminating: true},
{name: "terminating custom finalizer uses background", finalizer: "example.com/cleanup", terminating: true, wantBackground: true},
{name: "terminating without finalizers uses background", terminating: true, wantBackground: true},
{name: "live orphan", finalizer: metav1.FinalizerOrphanDependents},
} {
for _, force := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/force=%v", tt.name, force), func(t *testing.T) {
job := &unstructured.Unstructured{}
job.SetAPIVersion("batch/v1")
job.SetKind("Job")
job.SetNamespace("team")
job.SetName("nightly")
if tt.terminating {
now := metav1.Now()
job.SetDeletionTimestamp(&now)
}
if tt.finalizer != "" {
job.SetFinalizers([]string{tt.finalizer})
}
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), job)
var deletes []clienttesting.DeleteActionImpl
dyn.PrependReactor("delete", "jobs", func(a clienttesting.Action) (bool, runtime.Object, error) {
deletes = append(deletes, a.(clienttesting.DeleteActionImpl))
return false, nil, nil
})
err := NewWorkloadManager(dyn, disc).DeleteResource(context.Background(), DeleteResourceOptions{
Kind: "Job", Group: "batch", Namespace: "team", Name: "nightly", Force: force,
})
if err != nil {
t.Fatal(err)
}
if len(deletes) != 1 {
t.Fatalf("%d delete requests, want 1", len(deletes))
}
got := deletes[0].DeleteOptions
if tt.wantBackground || force {
if got.PropagationPolicy == nil || *got.PropagationPolicy != metav1.DeletePropagationBackground {
t.Errorf("propagationPolicy = %v, want Background", got.PropagationPolicy)
}
} else if got.PropagationPolicy != nil {
t.Errorf("propagationPolicy = %v, want nil", *got.PropagationPolicy)
}
if force {
if got.GracePeriodSeconds == nil || *got.GracePeriodSeconds != 0 {
t.Errorf("gracePeriodSeconds = %v, want 0", got.GracePeriodSeconds)
}
} else if got.GracePeriodSeconds != nil {
t.Errorf("gracePeriodSeconds = %v, want nil", got.GracePeriodSeconds)
}
})
}
}
}
func TestDeleteResourceReadFailureDoesNotBlockDelete(t *testing.T) {
gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}
disc, err := NewResourceDiscovery(fake.NewSimpleClientset().Discovery())
if err != nil {
t.Fatal(err)
}
disc.AddAPIResource(APIResource{Group: "batch", Version: "v1", Kind: "Job", Name: "jobs", Namespaced: true, Verbs: []string{"delete"}})
for _, tt := range []struct {
name string
err error
}{
{"forbidden", apierrors.NewForbidden(gvr.GroupResource(), "nightly", errors.New("read denied"))},
{"not found", apierrors.NewNotFound(gvr.GroupResource(), "nightly")},
{"other", errors.New("read failed")},
} {
for _, force := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/force=%v", tt.name, force), func(t *testing.T) {
job := &unstructured.Unstructured{}
job.SetAPIVersion("batch/v1")
job.SetKind("Job")
job.SetNamespace("team")
job.SetName("nightly")
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), job)
reads := 0
dyn.PrependReactor("get", "jobs", func(a clienttesting.Action) (bool, runtime.Object, error) {
reads++
return true, nil, tt.err
})
var deletes []clienttesting.DeleteActionImpl
dyn.PrependReactor("delete", "jobs", func(a clienttesting.Action) (bool, runtime.Object, error) {
if reads == 0 {
t.Error("delete sent without attempting a read")
}
deletes = append(deletes, a.(clienttesting.DeleteActionImpl))
return false, nil, nil
})
err := NewWorkloadManager(dyn, disc).DeleteResource(context.Background(), DeleteResourceOptions{
Kind: "Job", Group: "batch", Namespace: "team", Name: "nightly", Force: force,
})
if err != nil {
t.Fatal(err)
}
if len(deletes) != 1 {
t.Fatalf("%d delete requests, want 1", len(deletes))
}
got := deletes[0].DeleteOptions
if got.PropagationPolicy == nil || *got.PropagationPolicy != metav1.DeletePropagationBackground {
t.Errorf("propagationPolicy = %v, want Background", got.PropagationPolicy)
}
})
}
}
}
func TestForceDeleteStopsWhenFinalizersCannotBeStripped(t *testing.T) {
apps := schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
app := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "argoproj.io/v1alpha1", "kind": "Application",
"metadata": map[string]any{"namespace": "argocd", "name": "shop", "finalizers": []any{"resources-finalizer.argocd.argoproj.io"}},
}}
client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{apps: "ApplicationList"}, app)
client.PrependReactor("patch", "applications", func(clienttesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewInternalError(fmt.Errorf("etcd timeout"))
})
deleted := false
client.PrependReactor("delete", "applications", func(clienttesting.Action) (bool, runtime.Object, error) {
deleted = true
return false, nil, nil
})
discovery := &ResourceDiscovery{resourceMap: make(map[string]APIResource), gvrMap: make(map[string]schema.GroupVersionResource)}
discovery.AddAPIResource(APIResource{Group: "argoproj.io", Version: "v1alpha1", Kind: "Application", Name: "applications", Namespaced: true, IsCRD: true, Verbs: []string{"delete", "patch"}})
err := NewWorkloadManager(client, discovery).DeleteResource(context.Background(), DeleteResourceOptions{Kind: "Application", Group: "argoproj.io", Namespace: "argocd", Name: "shop", Force: true})
if err == nil || deleted {
t.Fatalf("err=%v deleted=%v; want an error and no delete", err, deleted)
}
}
func TestForceDeleteSkipsTheFinalizerPatchWhenThereAreNone(t *testing.T) {
pods := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
pod := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1", "kind": "Pod",
"metadata": map[string]any{"namespace": "team", "name": "stuck"},
}}
client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{pods: "PodList"}, pod)
client.PrependReactor("patch", "pods", func(clienttesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewInternalError(fmt.Errorf("admission webhook denied the request"))
})
discovery := &ResourceDiscovery{resourceMap: make(map[string]APIResource), gvrMap: make(map[string]schema.GroupVersionResource)}
discovery.AddAPIResource(APIResource{Version: "v1", Kind: "Pod", Name: "pods", Namespaced: true, Verbs: []string{"delete", "patch", "get"}})
if err := NewWorkloadManager(client, discovery).DeleteResource(context.Background(), DeleteResourceOptions{Kind: "Pod", Namespace: "team", Name: "stuck", Force: true}); err != nil {
t.Fatalf("force delete of a Pod without finalizers failed on a rejected patch: %v", err)
}
}
func TestDeleteResourceAcceptsPendingFinalizers(t *testing.T) {
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "held", "namespace": "demo", "uid": "original"}}}
obj.SetFinalizers([]string{"example.com/cleanup"})
now := metav1.Now()
obj.SetDeletionTimestamp(&now)
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
dyn.PrependReactor("delete", "configmaps", func(a clienttesting.Action) (bool, runtime.Object, error) { return true, nil, nil })
mgr := NewWorkloadManager(dyn, stubDiscovery(t, "ConfigMap", gvr))
if err := mgr.DeleteResource(context.Background(), DeleteResourceOptions{Kind: "ConfigMap", Namespace: "demo", Name: "held"}); err != nil {
t.Fatalf("accepted deletion returned error: %v", err)
}
for _, a := range dyn.Actions() {
if a.GetVerb() == "patch" {
t.Fatal("normal delete stripped finalizers")
}
}
}
func TestDeleteResourceResultObservesWithoutFailingAcceptedDelete(t *testing.T) {
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
for _, tc := range []struct {
name string
readError error
}{
{name: "pending finalizers"},
{name: "read forbidden", readError: apierrors.NewForbidden(gvr.GroupResource(), "held", fmt.Errorf("no get permission"))},
{name: "already gone", readError: apierrors.NewNotFound(gvr.GroupResource(), "held")},
} {
t.Run(tc.name, func(t *testing.T) {
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "held", "namespace": "demo", "uid": "original"}}}
now := metav1.Now()
obj.SetDeletionTimestamp(&now)
obj.SetFinalizers([]string{"example.com/cleanup"})
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
dyn.PrependReactor("delete", "configmaps", func(clienttesting.Action) (bool, runtime.Object, error) { return true, nil, nil })
if tc.readError != nil {
gets := 0
dyn.PrependReactor("get", "configmaps", func(clienttesting.Action) (bool, runtime.Object, error) {
gets++
if gets == 1 {
return false, nil, nil
}
return true, nil, tc.readError
})
}
result, err := NewWorkloadManager(dyn, stubDiscovery(t, "ConfigMap", gvr)).DeleteResourceWithResult(context.Background(), DeleteResourceOptions{Kind: "ConfigMap", Namespace: "demo", Name: "held"})
if err != nil {
t.Fatalf("accepted delete failed: %v", err)
}
if tc.readError == nil && (result.DeletionTimestamp == nil || len(result.PendingFinalizers) != 1) {
t.Fatalf("missing observation: %+v", result)
}
if apierrors.IsForbidden(tc.readError) && result.ObservationError == "" {
t.Fatal("missing read error observation")
}
if apierrors.IsNotFound(tc.readError) && (result.ObservationError != "" || result.DeletionTimestamp != nil) {
t.Fatalf("gone observation: %+v", result)
}
})
}
}
func TestObserveResourceDeletionIgnoresReplacement(t *testing.T) {
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "held", "namespace": "demo", "uid": "replacement"}}}
now := metav1.Now()
obj.SetDeletionTimestamp(&now)
obj.SetFinalizers([]string{"example.com/cleanup"})
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
result := ObserveResourceDeletion(context.Background(), dyn.Resource(gvr).Namespace("demo"), "held", "original")
if result.DeletionTimestamp != nil || len(result.PendingFinalizers) > 0 {
t.Fatalf("observed replacement: %+v", result)
}
}
func TestDeleteResourceResultDoesNotObserveReplacement(t *testing.T) {
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
for _, readDenied := range []bool{false, true} {
t.Run(fmt.Sprintf("read denied=%t", readDenied), func(t *testing.T) {
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "held", "namespace": "demo", "uid": "original"}}}
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
deleted, gets := false, 0
dyn.PrependReactor("get", "configmaps", func(clienttesting.Action) (bool, runtime.Object, error) {
gets++
if readDenied && !deleted {
return true, nil, apierrors.NewForbidden(gvr.GroupResource(), "held", fmt.Errorf("no get permission"))
}
return false, nil, nil
})
dyn.PrependReactor("delete", "configmaps", func(clienttesting.Action) (bool, runtime.Object, error) {
deleted = true
replacement := obj.DeepCopy()
replacement.SetUID("replacement")
now := metav1.Now()
replacement.SetDeletionTimestamp(&now)
replacement.SetFinalizers([]string{"example.com/replacement-cleanup"})
return true, nil, dyn.Tracker().Update(gvr, replacement, "demo")
})
result, err := NewWorkloadManager(dyn, stubDiscovery(t, "ConfigMap", gvr)).DeleteResourceWithResult(context.Background(), DeleteResourceOptions{Kind: "ConfigMap", Namespace: "demo", Name: "held"})
if err != nil || !deleted {
t.Fatalf("delete-only caller could not delete: result=%+v err=%v", result, err)
}
if result.DeletionTimestamp != nil || len(result.PendingFinalizers) != 0 {
t.Fatalf("observed replacement as deleted object: %+v", result)
}
if readDenied {
if gets != 1 || result.ObservationError == "" {
t.Fatalf("unknown UID must skip observation: gets=%d result=%+v", gets, result)
}
} else if gets != 2 || result.ObservationError != "" {
t.Fatalf("known UID observation: gets=%d result=%+v", gets, result)
}
})
}
}
// TestDeleteResourceSendsBackgroundPropagation pins the delete request to
// kubectl's: an explicit background policy, so a Job's Pods go with it
// instead of being orphaned by the API server's legacy default for Jobs.
func TestObserveResourceDeletionDoesNotClaimCompletionWhilePresent(t *testing.T) {
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "held", "namespace": "demo", "uid": "original"}}}
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
result := ObserveResourceDeletion(context.Background(), dyn.Resource(gvr).Namespace("demo"), "held", "original")
if result.ObservationError == "" || result.DeletionTimestamp != nil {
t.Fatalf("unconfirmed object reported gone: %+v", result)
}
}
func TestDeleteResourceBindsObservedUID(t *testing.T) {
gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
for _, force := range []bool{false, true} {
obj := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]any{"name": "web", "namespace": "demo", "uid": "original", "resourceVersion": "1"}}}
if force {
obj.SetFinalizers([]string{"hold"})
}
dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), obj)
dyn.PrependReactor("delete", "deployments", func(a clienttesting.Action) (bool, runtime.Object, error) {
opts := a.(clienttesting.DeleteAction).GetDeleteOptions()
if opts.Preconditions == nil || opts.Preconditions.UID == nil || *opts.Preconditions.UID != "original" {
t.Fatalf("unbound delete: %+v", opts)
}
return false, nil, nil
})
dyn.PrependReactor("patch", "deployments", func(a clienttesting.Action) (bool, runtime.Object, error) {
var patch map[string]any
if err := json.Unmarshal(a.(clienttesting.PatchAction).GetPatch(), &patch); err != nil {
t.Fatal(err)
}
m := patch["metadata"].(map[string]any)
if m["uid"] != "original" || m["resourceVersion"] != "1" {
t.Fatalf("unbound finalizer patch: %+v", m)
}
return false, nil, nil
})
discovery := stubDiscovery(t, "Deployment", gvr)
discovery.AddAPIResource(APIResource{Group: "apps", Version: "v1", Kind: "Deployment", Name: "deployments", Namespaced: true})
if err := NewWorkloadManager(dyn, discovery).DeleteResource(context.Background(), DeleteResourceOptions{Kind: "Deployment", Group: "apps", Namespace: "demo", Name: "web", Force: force}); err != nil {
t.Fatal(err)
}
}
}