// Copyright 2020-2023 Project Capsule Authors. // SPDX-License-Identifier: Apache-2.0 package e2e import ( "context" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/api/rbac" ) var _ = Describe("terminating namespace with guardrails", Ordered, Label("namespace", "termination"), func() { ctx := context.TODO() tnt := &capsulev1beta2.Tenant{ ObjectMeta: metav1.ObjectMeta{ Name: "e2e-termination", Labels: map[string]string{ "env": "e2e", }, }, Spec: capsulev1beta2.TenantSpec{ Owners: rbac.OwnerListSpec{ { CoreOwnerSpec: rbac.CoreOwnerSpec{ UserSpec: rbac.UserSpec{ Name: "e2e-termination", Kind: "User", }, }, }, }, }, } var ( nsName string podKey types.NamespacedName ) JustBeforeEach(func() { EventuallyCreation(func() error { tnt.ResourceVersion = "" return k8sClient.Create(ctx, tnt) }).Should(Succeed()) TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval) ns := NewNamespace("", map[string]string{ meta.TenantLabel: tnt.GetName(), }) nsName = ns.GetName() NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed()) NamespaceIsPartOfTenant(tnt, ns).Should(Succeed()) // Create a pod with a finalizer so the namespace can't complete deletion pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "finalizer-pod", Namespace: nsName, Finalizers: []string{"e2e.capsule.io/block-delete"}, }, Spec: corev1.PodSpec{ SecurityContext: nobodyPodSecurityContext(), Containers: []corev1.Container{ { Name: "pause", Image: "registry.k8s.io/pause:3.9", Command: []string{"/pause"}, SecurityContext: restrictedContainerSecurityContext(), }, }, }, } podKey = types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace} EventuallyCreation(func() error { return k8sClient.Create(ctx, pod) }).Should(Succeed()) }) JustAfterEach(func() { EventuallyDeletion(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podKey.Name, Namespace: podKey.Namespace}}) EventuallyDeletion(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}) EventuallyDeletionWithoutPodCleanup(tnt) }) It("keeps managed rolebindings during namespace termination and cleans up after finalizer removal", func() { By("deleting the namespace (it should get stuck terminating due to pod finalizer)", func() { ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}} Expect(k8sClient.Delete(ctx, ns)).To(Succeed()) }) By("verifying namespace is terminating", func() { Eventually(func(g Gomega) { ns := &corev1.Namespace{} err := k8sClient.Get(ctx, types.NamespacedName{Name: nsName}, ns) g.Expect(err).ToNot(HaveOccurred()) g.Expect(ns.DeletionTimestamp).ToNot(BeNil(), "namespace should have deletionTimestamp") }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) By("while namespace is terminating, verify rolebindings are still present", func() { Consistently(func(g Gomega) { // Namespace likely still exists during this window ns := &corev1.Namespace{} err := k8sClient.Get(ctx, types.NamespacedName{Name: nsName}, ns) g.Expect(err).ToNot(HaveOccurred()) VerifyTenantRoleBindings(tnt) }, 10*time.Second, defaultPollInterval).Should(Succeed()) }) By("while namespace is terminating, verify tenant still exists and has controller finalizer", func() { Consistently(func(g Gomega) { cur := &capsulev1beta2.Tenant{} g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, cur)).To(Succeed()) g.Expect(controllerutil.ContainsFinalizer(cur, meta.ControllerFinalizer)).To(BeTrue(), "tenant should still have controller finalizer while a namespace is terminating", ) }, 10*time.Second, defaultPollInterval).Should(Succeed()) }) By("removing the pod finalizer to unblock namespace deletion", func() { Eventually(func(g Gomega) error { p := &corev1.Pod{} if err := k8sClient.Get(ctx, podKey, p); err != nil { // If it's already gone, we're done if apierrors.IsNotFound(err) { return nil } return err } // Already cleared if len(p.Finalizers) == 0 { return nil } p.Finalizers = nil return k8sClient.Update(ctx, p) }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) By("verifying the pod is eventually deleted", func() { Eventually(func() bool { p := &corev1.Pod{} err := k8sClient.Get(ctx, podKey, p) return apierrors.IsNotFound(err) }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue(), "expected pod %s/%s to be deleted", podKey.Namespace, podKey.Name, ) }) By("verifying the namespace is eventually deleted", func() { Eventually(func() bool { ns := &corev1.Namespace{} err := k8sClient.Get(ctx, types.NamespacedName{Name: nsName}, ns) return apierrors.IsNotFound(err) }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue(), "expected namespace %q to be deleted", nsName, ) }) By("verifying tenant still exists (and finalizer cleanup depending on policy)", func() { Eventually(func(g Gomega) { cur := &capsulev1beta2.Tenant{} err := k8sClient.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, cur) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cur.Status.Size).Should(Equal(uint(0))) g.Expect(controllerutil.ContainsFinalizer(cur, meta.ControllerFinalizer)).To(BeFalse()) }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) }) })