Skip to content

Commit 0f40600

Browse files
committed
add e2e test to reproduce unexpected unmount after kubelet is restarted
Signed-off-by: carlory <baofa.fan@daocloud.io>
1 parent 9bf60d0 commit 0f40600

File tree

1 file changed

+122
-0
lines changed

1 file changed

+122
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
"os"
23+
"os/exec"
24+
"strings"
25+
26+
"github.com/onsi/ginkgo/v2"
27+
"github.com/onsi/gomega"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/kubernetes/test/e2e/feature"
30+
"k8s.io/kubernetes/test/e2e/framework"
31+
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
32+
"k8s.io/kubernetes/test/e2e/storage/utils"
33+
admissionapi "k8s.io/pod-security-admission/api"
34+
)
35+
36+
var _ = utils.SIGDescribe("CSI Mock when kubelet restart", feature.Kind, framework.WithSerial(), framework.WithDisruptive(), func() {
37+
f := framework.NewDefaultFramework("csi-mock-when-kubelet-restart")
38+
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
39+
m := newMockDriverSetup(f)
40+
41+
ginkgo.It("should not umount volume when the pvc is terminating but still used by a running pod", func(ctx context.Context) {
42+
m.init(ctx, testParameters{
43+
registerDriver: true,
44+
})
45+
ginkgo.DeferCleanup(m.cleanup)
46+
47+
ginkgo.By("Creating a Pod with a PVC backed by a CSI volume")
48+
_, pvc, pod := m.createPod(ctx, pvcReference)
49+
50+
ginkgo.By("Waiting for the Pod to be running")
51+
err := e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)
52+
framework.ExpectNoError(err, "failed to wait for pod %s to be running", pod.Name)
53+
54+
ginkgo.By("Deleting the PVC")
55+
err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{})
56+
framework.ExpectNoError(err, "failed to delete PVC %s", pvc.Name)
57+
58+
ginkgo.By("Restarting kubelet")
59+
err = stopKindKubelet(ctx)
60+
framework.ExpectNoError(err, "failed to stop kubelet")
61+
err = startKindKubelet(ctx)
62+
framework.ExpectNoError(err, "failed to start kubelet")
63+
64+
ginkgo.By("Verifying the PVC is terminating during kubelet restart")
65+
pvc, err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(ctx, pvc.Name, metav1.GetOptions{})
66+
framework.ExpectNoError(err, "failed to get PVC %s", pvc.Name)
67+
gomega.Expect(pvc.DeletionTimestamp).NotTo(gomega.BeNil(), "PVC %s should have deletion timestamp", pvc.Name)
68+
69+
// FIXME: the expected behavior is no NodeUnpublishVolume call is made during kubelet restart
70+
ginkgo.By(fmt.Sprintf("Verifying that the driver received NodeUnpublishVolume call for PVC %s", pvc.Name))
71+
gomega.Eventually(ctx, m.driver.GetCalls).
72+
WithPolling(framework.Poll).
73+
WithTimeout(framework.RestartNodeReadyAgainTimeout).
74+
Should(gomega.ContainElement(gomega.HaveField("Method", gomega.Equal("NodeUnpublishVolume"))))
75+
76+
// ginkgo.By(fmt.Sprintf("Verifying that the driver didn't receive NodeUnpublishVolume call for PVC %s", pvc.Name))
77+
// gomega.Consistently(ctx, m.driver.GetCalls).
78+
// WithPolling(framework.Poll).
79+
// WithTimeout(framework.ClaimProvisionShortTimeout).
80+
// ShouldNot(gomega.ContainElement(gomega.HaveField("Method", gomega.Equal("NodeUnpublishVolume"))))
81+
82+
// ginkgo.By("Verifying the Pod is still running")
83+
// err = e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)
84+
// framework.ExpectNoError(err, "failed to wait for pod %s to be running", pod.Name)
85+
})
86+
})
87+
88+
func stopKindKubelet(ctx context.Context) error {
89+
return kubeletExec("systemctl", "stop", "kubelet")
90+
}
91+
92+
func startKindKubelet(ctx context.Context) error {
93+
return kubeletExec("systemctl", "start", "kubelet")
94+
}
95+
96+
// Run a command in container with kubelet (and the whole control plane as containers)
97+
func kubeletExec(command ...string) error {
98+
containerName := getKindContainerName()
99+
args := []string{"exec", containerName}
100+
args = append(args, command...)
101+
cmd := exec.Command("docker", args...)
102+
103+
out, err := cmd.CombinedOutput()
104+
if err != nil {
105+
return fmt.Errorf("command %q failed: %v\noutput:%s", prettyCmd(cmd), err, string(out))
106+
}
107+
108+
framework.Logf("command %q succeeded:\n%s", prettyCmd(cmd), string(out))
109+
return nil
110+
}
111+
112+
func getKindContainerName() string {
113+
clusterName := os.Getenv("KIND_CLUSTER_NAME")
114+
if clusterName == "" {
115+
clusterName = "kind"
116+
}
117+
return clusterName + "-control-plane"
118+
}
119+
120+
func prettyCmd(cmd *exec.Cmd) string {
121+
return fmt.Sprintf("%s %s", cmd.Path, strings.Join(cmd.Args, " "))
122+
}

0 commit comments

Comments
 (0)