diff --git a/pkg/controller/podgc/gc_controller_test.go b/pkg/controller/podgc/gc_controller_test.go index 1980703393a..4bc11255613 100644 --- a/pkg/controller/podgc/gc_controller_test.go +++ b/pkg/controller/podgc/gc_controller_test.go @@ -18,14 +18,18 @@ package podgc import ( "context" + "encoding/json" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/wait" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/informers" @@ -43,6 +47,7 @@ import ( "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/kubelet/eviction" testingclock "k8s.io/utils/clock/testing" + "k8s.io/utils/pointer" ) func alwaysReady() bool { return true } @@ -671,6 +676,128 @@ func TestGCTerminating(t *testing.T) { testDeletingPodsMetrics(t, 7, metrics.PodGCReasonTerminatingOutOfService) } +func TestGCInspectingPatchedPodBeforeDeletion(t *testing.T) { + testCases := []struct { + name string + pod *v1.Pod + expectedPatchedPod *v1.Pod + expectedDeleteAction *clienttesting.DeleteActionImpl + }{ + { + name: "orphaned pod should have DisruptionTarget condition added before deletion", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "testPod", + }, + Spec: v1.PodSpec{ + NodeName: "deletedNode", + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + Conditions: []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + expectedPatchedPod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "testPod", + }, + Spec: v1.PodSpec{ + NodeName: "deletedNode", + }, + Status: v1.PodStatus{ + Phase: v1.PodFailed, + Conditions: []v1.PodCondition{ + { + Type: v1.DisruptionTarget, + Status: v1.ConditionTrue, + Reason: "DeletionByPodGC", + Message: "PodGC: node no longer exists", + }, + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + expectedDeleteAction: &clienttesting.DeleteActionImpl{ + Name: "testPod", + DeleteOptions: metav1.DeleteOptions{GracePeriodSeconds: pointer.Int64(0)}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + _, ctx := ktesting.NewTestContext(t) + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDisruptionConditions, true)() + + pods := []*v1.Pod{test.pod} + + client := setupNewSimpleClient(nil, pods) + gcc, podInformer, _ := NewFromClient(ctx, client, -1) + gcc.quarantineTime = time.Duration(-1) + podInformer.Informer().GetStore().Add(test.pod) + gcc.gc(ctx) + + actions := client.Actions() + + var patchAction clienttesting.PatchAction + var deleteAction clienttesting.DeleteAction + + for _, action := range actions { + if action.GetVerb() == "patch" { + patchAction = action.(clienttesting.PatchAction) + } + + if action.GetVerb() == "delete" { + deleteAction = action.(clienttesting.DeleteAction) + } + } + + if patchAction != nil && test.expectedPatchedPod == nil { + t.Fatalf("Pod was pactched but expectedPatchedPod is nil") + } + if test.expectedPatchedPod != nil { + patchedPodBytes := patchAction.GetPatch() + originalPod, err := json.Marshal(test.pod) + if err != nil { + t.Fatalf("Failed to marshal original pod %#v: %v", originalPod, err) + } + updated, err := strategicpatch.StrategicMergePatch(originalPod, patchedPodBytes, v1.Pod{}) + if err != nil { + t.Fatalf("Failed to apply strategic merge patch %q on pod %#v: %v", patchedPodBytes, originalPod, err) + } + + updatedPod := &v1.Pod{} + if err := json.Unmarshal(updated, updatedPod); err != nil { + t.Fatalf("Failed to unmarshal updated pod %q: %v", updated, err) + } + + if diff := cmp.Diff(test.expectedPatchedPod, updatedPod, cmpopts.IgnoreFields(v1.Pod{}, "TypeMeta"), cmpopts.IgnoreFields(v1.PodCondition{}, "LastTransitionTime")); diff != "" { + t.Fatalf("Unexpected diff on pod (-want,+got):\n%s", diff) + } + } + + if deleteAction != nil && test.expectedDeleteAction == nil { + t.Fatalf("Pod was deleted but expectedDeleteAction is nil") + } + if test.expectedDeleteAction != nil { + if diff := cmp.Diff(*test.expectedDeleteAction, deleteAction, cmpopts.IgnoreFields(clienttesting.DeleteActionImpl{}, "ActionImpl")); diff != "" { + t.Fatalf("Unexpected diff on deleteAction (-want,+got):\n%s", diff) + } + } + }) + } +} + func verifyDeletedAndPatchedPods(t *testing.T, client *fake.Clientset, wantDeletedPodNames, wantPatchedPodNames sets.String) { t.Helper() deletedPodNames := getDeletedPodNames(client)