Skip to content

Commit 5227bad

Browse files
authored
Merge pull request kubernetes#130335 from carlory/fix-handle-terminating-pvc-when-kubelet-rebuild-dsw
Fix kubelet restart unmounts volumes of running pods if the referenced PVC is being deleted by the user
2 parents 0f2bde7 + aab0839 commit 5227bad

File tree

2 files changed

+113
-6
lines changed

2 files changed

+113
-6
lines changed

pkg/kubelet/volumemanager/populator/desired_state_of_world_populator.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"context"
2525
"errors"
2626
"fmt"
27+
"slices"
2728
"sync"
2829
"time"
2930

@@ -528,15 +529,21 @@ func (dswp *desiredStateOfWorldPopulator) getPVCExtractPV(
528529
return nil, fmt.Errorf("failed to fetch PVC from API server: %v", err)
529530
}
530531

531-
// Pods that uses a PVC that is being deleted must not be started.
532+
// Pods that uses a PVC that is being deleted and not protected by
533+
// kubernetes.io/pvc-protection must not be started.
532534
//
533-
// In case an old kubelet is running without this check or some kubelets
534-
// have this feature disabled, the worst that can happen is that such
535-
// pod is scheduled. This was the default behavior in 1.8 and earlier
536-
// and users should not be that surprised.
535+
// 1) In case an old kubelet is running without this check, the worst
536+
// that can happen is that such pod is scheduled. This was the default
537+
// behavior in 1.8 and earlier and users should not be that surprised.
537538
// It should happen only in very rare case when scheduler schedules
538539
// a pod and user deletes a PVC that's used by it at the same time.
539-
if pvc.ObjectMeta.DeletionTimestamp != nil {
540+
//
541+
// 2) Adding a check for kubernetes.io/pvc-protection here to prevent
542+
// the existing running pods from being affected during the rebuild of
543+
// the desired state of the world cache when the kubelet is restarted.
544+
// It is safe for kubelet to add this check here because the PVC will
545+
// be stuck in Terminating state until the pod is deleted.
546+
if pvc.ObjectMeta.DeletionTimestamp != nil && !slices.Contains(pvc.Finalizers, util.PVCProtectionFinalizer) {
540547
return nil, errors.New("PVC is being deleted")
541548
}
542549

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package csimock
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/onsi/ginkgo/v2"
24+
"github.com/onsi/gomega"
25+
apierrors "k8s.io/apimachinery/pkg/api/errors"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/kubernetes/test/e2e/framework"
28+
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
29+
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
30+
"k8s.io/kubernetes/test/e2e/storage/drivers"
31+
"k8s.io/kubernetes/test/e2e/storage/utils"
32+
admissionapi "k8s.io/pod-security-admission/api"
33+
)
34+
35+
var _ = utils.SIGDescribe("CSI Mock when kubelet restart", framework.WithSerial(), framework.WithDisruptive(), func() {
36+
f := framework.NewDefaultFramework("csi-mock-when-kubelet-restart")
37+
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
38+
m := newMockDriverSetup(f)
39+
40+
ginkgo.BeforeEach(func() {
41+
// These tests requires SSH to nodes, so the provider check should be identical to there
42+
// (the limiting factor is the implementation of util.go's e2essh.GetSigner(...)).
43+
44+
// Cluster must support node reboot
45+
e2eskipper.SkipUnlessProviderIs(framework.ProvidersWithSSH...)
46+
e2eskipper.SkipUnlessSSHKeyPresent()
47+
})
48+
49+
ginkgo.It("should not umount volume when the pvc is terminating but still used by a running pod", func(ctx context.Context) {
50+
51+
m.init(ctx, testParameters{
52+
registerDriver: true,
53+
})
54+
ginkgo.DeferCleanup(m.cleanup)
55+
56+
ginkgo.By("Creating a Pod with a PVC backed by a CSI volume")
57+
_, pvc, pod := m.createPod(ctx, pvcReference)
58+
59+
ginkgo.By("Waiting for the Pod to be running")
60+
err := e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)
61+
framework.ExpectNoError(err, "failed to wait for pod %s to be running", pod.Name)
62+
pod, err = f.ClientSet.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
63+
framework.ExpectNoError(err, "failed to get pod %s", pod.Name)
64+
65+
ginkgo.By("Deleting the PVC")
66+
err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{})
67+
framework.ExpectNoError(err, "failed to delete PVC %s", pvc.Name)
68+
69+
ginkgo.By("Restarting kubelet")
70+
utils.KubeletCommand(ctx, utils.KRestart, f.ClientSet, pod)
71+
ginkgo.DeferCleanup(utils.KubeletCommand, utils.KStart, f.ClientSet, pod)
72+
73+
ginkgo.By("Verifying the PVC is terminating during kubelet restart")
74+
pvc, err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(ctx, pvc.Name, metav1.GetOptions{})
75+
framework.ExpectNoError(err, "failed to get PVC %s", pvc.Name)
76+
gomega.Expect(pvc.DeletionTimestamp).NotTo(gomega.BeNil(), "PVC %s should have deletion timestamp", pvc.Name)
77+
78+
ginkgo.By(fmt.Sprintf("Verifying that the driver didn't receive NodeUnpublishVolume call for PVC %s", pvc.Name))
79+
gomega.Consistently(ctx,
80+
func(ctx context.Context) []drivers.MockCSICall {
81+
calls, err := m.driver.GetCalls(ctx)
82+
if err != nil {
83+
if apierrors.IsUnexpectedServerError(err) {
84+
// kubelet might not be ready yet when getting the calls
85+
gomega.TryAgainAfter(framework.Poll).Wrap(err).Now()
86+
return nil
87+
}
88+
return nil
89+
}
90+
return calls
91+
}).
92+
WithPolling(framework.Poll).
93+
WithTimeout(framework.ClaimProvisionShortTimeout).
94+
ShouldNot(gomega.ContainElement(gomega.HaveField("Method", gomega.Equal("NodeUnpublishVolume"))))
95+
96+
ginkgo.By("Verifying the Pod is still running")
97+
err = e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)
98+
framework.ExpectNoError(err, "failed to wait for pod %s to be running", pod.Name)
99+
})
100+
})

0 commit comments

Comments
 (0)