diff --git a/pkg/webhook/util/deletionprotection/deletion_protection.go b/pkg/webhook/util/deletionprotection/deletion_protection.go index 0a7cb6c6c1..e8654f44f5 100644 --- a/pkg/webhook/util/deletionprotection/deletion_protection.go +++ b/pkg/webhook/util/deletionprotection/deletion_protection.go @@ -72,16 +72,19 @@ func ValidateNamespaceDeletion(c client.Client, namespace *v1.Namespace) error { return fmt.Errorf("forbidden by ResourcesProtectionDeletion for %s=%s and active pods %d>0", policyv1alpha1.DeletionProtectionKey, val, activeCount) } - pvc := v1.PersistentVolumeClaimList{} - if err := c.List(context.TODO(), &pvc, client.InNamespace(namespace.Name)); err != nil { + pvcs := v1.PersistentVolumeClaimList{} + if err := c.List(context.TODO(), &pvcs, client.InNamespace(namespace.Name)); err != nil { return fmt.Errorf("forbidden by ResourcesProtectionDeletion for list pvc error: %v", err) } - var pvcCount int - for range pvc.Items { - pvcCount++ + var boundCount int + for i := range pvcs.Items { + pvc := &pvcs.Items[i] + if pvc.Status.Phase == v1.ClaimBound { + boundCount++ + } } - if pvcCount > 0 { - return fmt.Errorf("forbidden by ResourcesProtectionDeletion for %s=%s and existing pvc %d>0", policyv1alpha1.DeletionProtectionKey, val, pvcCount) + if boundCount > 0 { + return fmt.Errorf("forbidden by ResourcesProtectionDeletion for %s=%s and \"Bound\" status pvc %d>0", policyv1alpha1.DeletionProtectionKey, val, boundCount) } default: } diff --git a/test/e2e/policy/deletionprotection.go b/test/e2e/policy/deletionprotection.go index 5f829f88f8..461624b0fc 100644 --- a/test/e2e/policy/deletionprotection.go +++ b/test/e2e/policy/deletionprotection.go @@ -19,6 +19,7 @@ package policy import ( "context" "fmt" + "k8s.io/apimachinery/pkg/api/resource" "time" "github.com/onsi/ginkgo" @@ -111,6 +112,78 @@ var _ = SIGDescribe("DeletionProtection", func() { return cs.Status.Replicas }, 5*time.Second, time.Second).Should(gomega.Equal(int32(0))) + ginkgo.By("Create a PVC in this namespace") + pvcName := "pvc-" + randStr + storageClassName := "" + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: pvcName, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + StorageClassName: &storageClassName, + }, + } + pvc, err = c.CoreV1().PersistentVolumeClaims(ns.Name).Create(context.TODO(), pvc, metav1.CreateOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + ginkgo.By("Create a PV bound to the PVC just created") + pvName := "pv-" + randStr + pv := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvName, + }, + Spec: v1.PersistentVolumeSpec{ + Capacity: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + StorageClassName: "", + ClaimRef: &v1.ObjectReference{ + Namespace: ns.Name, + Name: pvcName, + }, + PersistentVolumeSource: v1.PersistentVolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/mnt", + }, + }, + }, + } + pv, err = framework.CreatePV(c, pv) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Eventually(func() bool { + pvc, err := c.CoreV1().PersistentVolumeClaims(ns.Name).Get(context.TODO(), pvcName, metav1.GetOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + return pvc.Status.Phase == v1.ClaimBound + }, 15*time.Second, time.Second).Should(gomega.BeTrue()) + + ginkgo.By("Delete the namespace should be rejected") + err = c.CoreV1().Namespaces().Delete(context.TODO(), ns.Name, metav1.DeleteOptions{}) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(err.Error()).Should(gomega.ContainSubstring(deleteForbiddenMessage)) + + ginkgo.By("Delete the PV bounded to PVC") + err = c.CoreV1().PersistentVolumes().Delete(context.TODO(), pvName, metav1.DeleteOptions{}) + _, err = c.CoreV1().PersistentVolumes().Patch(context.TODO(), pvName, types.StrategicMergePatchType, + []byte(fmt.Sprintf(`{"metadata":{"finalizers":null}}`)), metav1.PatchOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Eventually(func() bool { + pvc, err := c.CoreV1().PersistentVolumeClaims(ns.Name).Get(context.TODO(), pvcName, metav1.GetOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + return pvc.Status.Phase != v1.ClaimBound + }, 5*time.Second, time.Second).Should(gomega.BeTrue()) + time.Sleep(time.Second) ginkgo.By("Delete the namespace successfully") err = c.CoreV1().Namespaces().Delete(context.TODO(), ns.Name, metav1.DeleteOptions{})