Skip to content

Commit b38bf6c

Browse files
authored
Merge pull request kubernetes#130035 from cici37/nsDeletion
[KEP-5080]Ordered Namespace Deletion
2 parents 20cecae + e1b3c8f commit b38bf6c

File tree

6 files changed

+158
-0
lines changed

6 files changed

+158
-0
lines changed

pkg/controller/namespace/deletion/namespaced_resources_deleter.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ import (
3232
utilerrors "k8s.io/apimachinery/pkg/util/errors"
3333
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3434
"k8s.io/apimachinery/pkg/util/sets"
35+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3536
"k8s.io/client-go/discovery"
3637
v1clientset "k8s.io/client-go/kubernetes/typed/core/v1"
3738
"k8s.io/client-go/metadata"
39+
"k8s.io/kubernetes/pkg/features"
3840
)
3941

4042
// NamespacedResourcesDeleterInterface is the interface to delete a namespace with all resources in it.
@@ -526,6 +528,38 @@ func (d *namespacedResourcesDeleter) deleteAllContent(ctx context.Context, ns *v
526528
gvrToNumRemaining: map[schema.GroupVersionResource]int{},
527529
finalizersToNumRemaining: map[string]int{},
528530
}
531+
532+
if utilfeature.DefaultFeatureGate.Enabled(features.OrderedNamespaceDeletion) {
533+
// TODO: remove this log when the feature gate is enabled by default
534+
logger.V(5).Info("Namespace controller - OrderedNamespaceDeletion feature gate is enabled", "namespace", namespace)
535+
// Ensure all pods in the namespace are deleted first
536+
podsGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
537+
gvrDeletionMetadata, err := d.deleteAllContentForGroupVersionResource(ctx, podsGVR, namespace, namespaceDeletedAt)
538+
if err != nil {
539+
errs = append(errs, fmt.Errorf("failed to delete pods for namespace: %s, err: %w", namespace, err))
540+
conditionUpdater.ProcessDeleteContentErr(err)
541+
}
542+
if gvrDeletionMetadata.finalizerEstimateSeconds > estimate {
543+
estimate = gvrDeletionMetadata.finalizerEstimateSeconds
544+
}
545+
if gvrDeletionMetadata.numRemaining > 0 {
546+
numRemainingTotals.gvrToNumRemaining[podsGVR] = gvrDeletionMetadata.numRemaining
547+
for finalizer, numRemaining := range gvrDeletionMetadata.finalizersToNumRemaining {
548+
if numRemaining == 0 {
549+
continue
550+
}
551+
numRemainingTotals.finalizersToNumRemaining[finalizer] += numRemaining
552+
}
553+
}
554+
555+
// Check if any pods remain before proceeding to delete other resources
556+
if numRemainingTotals.gvrToNumRemaining[podsGVR] > 0 {
557+
logger.V(5).Info("Namespace controller - pods still remain, delaying deletion of other resources", "namespace", namespace)
558+
return estimate, utilerrors.NewAggregate(errs)
559+
}
560+
}
561+
562+
// Proceed with deleting other resources in the namespace
529563
for gvr := range groupVersionResources {
530564
gvrDeletionMetadata, err := d.deleteAllContentForGroupVersionResource(ctx, gvr, namespace, namespaceDeletedAt)
531565
if err != nil {

pkg/features/kube_features.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,12 @@ const (
436436
// Permits kubelet to run with swap enabled.
437437
NodeSwap featuregate.Feature = "NodeSwap"
438438

439+
// owner: @cici37
440+
// kep: https://kep.k8s.io/5080
441+
//
442+
// Enables ordered namespace deletion.
443+
OrderedNamespaceDeletion featuregate.Feature = "OrderedNamespaceDeletion"
444+
439445
// owner: @RomanBednar
440446
// kep: https://kep.k8s.io/3762
441447
//

pkg/features/versioned_kube_features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
548548
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
549549
},
550550

551+
OrderedNamespaceDeletion: {
552+
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
553+
},
554+
551555
PersistentVolumeLastPhaseTransitionTime: {
552556
{Version: version.MustParse("1.28"), Default: false, PreRelease: featuregate.Alpha},
553557
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.Beta},

test/e2e/apimachinery/namespace.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222
"fmt"
23+
"k8s.io/kubernetes/pkg/features"
2324
"strings"
2425
"sync"
2526
"time"
@@ -475,3 +476,106 @@ func unstructuredToNamespace(obj *unstructured.Unstructured) (*v1.Namespace, err
475476

476477
return ns, err
477478
}
479+
480+
var _ = SIGDescribe("OrderedNamespaceDeletion", func() {
481+
f := framework.NewDefaultFramework("namespacedeletion")
482+
f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
483+
484+
f.It("namespace deletion should delete pod first", feature.OrderedNamespaceDeletion, framework.WithFeatureGate(features.OrderedNamespaceDeletion), func(ctx context.Context) {
485+
ensurePodsAreRemovedFirstInOrderedNamespaceDeletion(ctx, f)
486+
})
487+
})
488+
489+
func ensurePodsAreRemovedFirstInOrderedNamespaceDeletion(ctx context.Context, f *framework.Framework) {
490+
ginkgo.By("Creating a test namespace")
491+
namespaceName := "nsdeletetest"
492+
namespace, err := f.CreateNamespace(ctx, namespaceName, nil)
493+
framework.ExpectNoError(err, "failed to create namespace: %s", namespaceName)
494+
nsName := namespace.Name
495+
496+
ginkgo.By("Waiting for a default service account to be provisioned in namespace")
497+
err = framework.WaitForDefaultServiceAccountInNamespace(ctx, f.ClientSet, nsName)
498+
framework.ExpectNoError(err, "failure while waiting for a default service account to be provisioned in namespace: %s", nsName)
499+
500+
ginkgo.By("Creating a pod with finalizer in the namespace")
501+
podName := "test-pod"
502+
pod := &v1.Pod{
503+
ObjectMeta: metav1.ObjectMeta{
504+
Name: podName,
505+
Finalizers: []string{
506+
"e2e.example.com/finalizer",
507+
},
508+
},
509+
Spec: v1.PodSpec{
510+
Containers: []v1.Container{
511+
{
512+
Name: "nginx",
513+
Image: imageutils.GetPauseImageName(),
514+
},
515+
},
516+
},
517+
}
518+
pod, err = f.ClientSet.CoreV1().Pods(nsName).Create(ctx, pod, metav1.CreateOptions{})
519+
framework.ExpectNoError(err, "failed to create pod %s in namespace: %s", podName, nsName)
520+
521+
ginkgo.By("Waiting for the pod to have running status")
522+
framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod))
523+
524+
configMapName := "test-configmap"
525+
ginkgo.By(fmt.Sprintf("Creating a configmap %q in namespace %q", configMapName, nsName))
526+
configMap := &v1.ConfigMap{
527+
ObjectMeta: metav1.ObjectMeta{
528+
Name: configMapName,
529+
Namespace: nsName,
530+
},
531+
Data: map[string]string{
532+
"key": "value",
533+
},
534+
}
535+
_, err = f.ClientSet.CoreV1().ConfigMaps(nsName).Create(ctx, configMap, metav1.CreateOptions{})
536+
framework.ExpectNoError(err, "failed to create configmap %q in namespace %q", configMapName, nsName)
537+
538+
ginkgo.By("Deleting the namespace")
539+
err = f.ClientSet.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})
540+
framework.ExpectNoError(err, "failed to delete namespace: %s", nsName)
541+
// wait 10 seconds to allow the namespace controller to process
542+
time.Sleep(10 * time.Second)
543+
ginkgo.By("the pod should be deleted before processing deletion for other resources")
544+
framework.ExpectNoError(wait.PollUntilContextTimeout(ctx, 2*time.Second, 60*time.Second, true,
545+
func(ctx context.Context) (bool, error) {
546+
_, err = f.ClientSet.CoreV1().ConfigMaps(nsName).Get(ctx, configMapName, metav1.GetOptions{})
547+
framework.ExpectNoError(err, "configmap %q should still exist in namespace %q", configMapName, nsName)
548+
// the pod should exist and has a deletionTimestamp set
549+
pod, err = f.ClientSet.CoreV1().Pods(nsName).Get(ctx, pod.Name, metav1.GetOptions{})
550+
framework.ExpectNoError(err, "failed to get pod %q in namespace %q", pod.Name, nsName)
551+
if pod.DeletionTimestamp == nil {
552+
framework.Failf("Pod %q in namespace %q does not have a metadata.deletionTimestamp set", pod.Name, nsName)
553+
}
554+
_, err = f.ClientSet.CoreV1().Namespaces().Get(ctx, nsName, metav1.GetOptions{})
555+
if err != nil && apierrors.IsNotFound(err) {
556+
return false, fmt.Errorf("namespace %s was deleted unexpectedly", nsName)
557+
}
558+
return true, nil
559+
}))
560+
561+
ginkgo.By(fmt.Sprintf("Removing finalizer from pod %q in namespace %q", podName, nsName))
562+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
563+
pod, err = f.ClientSet.CoreV1().Pods(nsName).Get(ctx, podName, metav1.GetOptions{})
564+
framework.ExpectNoError(err, "failed to get pod %q in namespace %q", pod.Name, nsName)
565+
pod.Finalizers = []string{}
566+
_, err = f.ClientSet.CoreV1().Pods(nsName).Update(ctx, pod, metav1.UpdateOptions{})
567+
return err
568+
})
569+
framework.ExpectNoError(err, "failed to update pod %q and remove finalizer in namespace %q", podName, nsName)
570+
571+
ginkgo.By("Waiting for the namespace to be removed.")
572+
maxWaitSeconds := int64(60) + *pod.Spec.TerminationGracePeriodSeconds
573+
framework.ExpectNoError(wait.PollUntilContextTimeout(ctx, 1*time.Second, time.Duration(maxWaitSeconds)*time.Second, true,
574+
func(ctx context.Context) (bool, error) {
575+
_, err = f.ClientSet.CoreV1().Namespaces().Get(ctx, namespace.Name, metav1.GetOptions{})
576+
if err != nil && apierrors.IsNotFound(err) {
577+
return true, nil
578+
}
579+
return false, nil
580+
}))
581+
}

test/e2e/feature/feature.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ var (
334334
// Tests aiming to verify oom_score functionality
335335
OOMScoreAdj = framework.WithFeature(framework.ValidFeatures.Add("OOMScoreAdj"))
336336

337+
// Owner: sig-api-machinery
338+
// Marks tests that enforce ordered namespace deletion.
339+
OrderedNamespaceDeletion = framework.WithFeature(framework.ValidFeatures.Add("OrderedNamespaceDeletion"))
340+
337341
// Owner: sig-node
338342
// Verify ProcMount feature.
339343
// Used in combination with user namespaces

test/featuregates_linter/test_data/versioned_feature_list.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,12 @@
866866
lockToDefault: false
867867
preRelease: Beta
868868
version: "1.24"
869+
- name: OrderedNamespaceDeletion
870+
versionedSpecs:
871+
- default: false
872+
lockToDefault: false
873+
preRelease: Alpha
874+
version: "1.33"
869875
- name: PersistentVolumeLastPhaseTransitionTime
870876
versionedSpecs:
871877
- default: false

0 commit comments

Comments
 (0)