Skip to content

Commit b2799bb

Browse files
authored
Merge pull request kubernetes#124690 from mowangdk/test/add_e2e_test_for_volume_health
chore: Add e2e test for NodeGetVolumeStats
2 parents 09f0259 + 2eeaebc commit b2799bb

File tree

6 files changed

+262
-29
lines changed

6 files changed

+262
-29
lines changed

test/e2e/feature/feature.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,19 @@ var (
395395
// TODO: document the feature (owning SIG, when to use this feature for a test)
396396
VolumeSourceXFS = framework.WithFeature(framework.ValidFeatures.Add("VolumeSourceXFS"))
397397

398+
// Ownerd by SIG Storage
399+
// kep: https://kep.k8s.io/1432
400+
// test-infra jobs:
401+
// - pull-kubernetes-e2e-storage-kind-alpha-features (need manual trigger)
402+
// - ci-kubernetes-e2e-storage-kind-alpha-features
403+
// When this label is added to a test, it means that the cluster must be created
404+
// with the feature-gate "CSIVolumeHealth=true".
405+
//
406+
// Once the feature is stable, this label should be removed and these tests will
407+
// be run by default on any cluster. The test-infra job also should be updated to
408+
// not focus on this feature anymore.
409+
CSIVolumeHealth = framework.WithFeature(framework.ValidFeatures.Add("CSIVolumeHealth"))
410+
398411
// TODO: document the feature (owning SIG, when to use this feature for a test)
399412
Vsphere = framework.WithFeature(framework.ValidFeatures.Add("vsphere"))
400413

test/e2e/storage/csimock/base.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const (
5656
csiPodUnschedulableTimeout = 5 * time.Minute
5757
csiResizeWaitPeriod = 5 * time.Minute
5858
csiVolumeAttachmentTimeout = 7 * time.Minute
59+
// how long to wait for GetVolumeStats
60+
csiNodeVolumeStatWaitPeriod = 2 * time.Minute
5961
// how long to wait for Resizing Condition on PVC to appear
6062
csiResizingConditionWait = 2 * time.Minute
6163

@@ -96,6 +98,7 @@ type testParameters struct {
9698
disableResizingOnDriver bool
9799
enableSnapshot bool
98100
enableVolumeMountGroup bool // enable the VOLUME_MOUNT_GROUP node capability in the CSI mock driver.
101+
enableNodeVolumeCondition bool
99102
hooks *drivers.Hooks
100103
tokenRequests []storagev1.TokenRequest
101104
requiresRepublish *bool
@@ -168,6 +171,7 @@ func (m *mockDriverSetup) init(ctx context.Context, tp testParameters) {
168171
DisableAttach: tp.disableAttach,
169172
EnableResizing: tp.enableResizing,
170173
EnableNodeExpansion: tp.enableNodeExpansion,
174+
EnableNodeVolumeCondition: tp.enableNodeVolumeCondition,
171175
EnableSnapshot: tp.enableSnapshot,
172176
EnableVolumeMountGroup: tp.enableVolumeMountGroup,
173177
TokenRequests: tp.tokenRequests,
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
Copyright 2024 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+
"strings"
22+
"time"
23+
24+
"google.golang.org/grpc/codes"
25+
26+
csipbv1 "github.com/container-storage-interface/spec/lib/go/csi"
27+
v1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/util/wait"
30+
"k8s.io/kubernetes/pkg/features"
31+
kubeletmetrics "k8s.io/kubernetes/pkg/kubelet/metrics"
32+
"k8s.io/kubernetes/test/e2e/feature"
33+
"k8s.io/kubernetes/test/e2e/framework"
34+
e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics"
35+
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
36+
e2epv "k8s.io/kubernetes/test/e2e/framework/pv"
37+
"k8s.io/kubernetes/test/e2e/storage/drivers"
38+
"k8s.io/kubernetes/test/e2e/storage/utils"
39+
admissionapi "k8s.io/pod-security-admission/api"
40+
41+
"github.com/onsi/ginkgo/v2"
42+
)
43+
44+
var _ = utils.SIGDescribe("CSI Mock Node Volume Health", feature.CSIVolumeHealth, framework.WithFeatureGate(features.CSIVolumeHealth), func() {
45+
f := framework.NewDefaultFramework("csi-mock-node-volume-health")
46+
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
47+
m := newMockDriverSetup(f)
48+
49+
f.Context("CSI Mock Node Volume Health", f.WithSlow(), func() {
50+
trackedCalls := []string{
51+
"NodeGetVolumeStats",
52+
}
53+
tests := []struct {
54+
name string
55+
expectedCalls []csiCall
56+
nodeVolumeConditionRequired bool
57+
nodeAbnormalVolumeCondition bool
58+
}{
59+
{
60+
name: "return normal volume stats",
61+
expectedCalls: []csiCall{
62+
{
63+
expectedMethod: "NodeGetVolumeStats",
64+
expectedError: codes.OK,
65+
},
66+
},
67+
nodeVolumeConditionRequired: true,
68+
nodeAbnormalVolumeCondition: false,
69+
},
70+
{
71+
name: "return normal volume stats without volume condition",
72+
expectedCalls: []csiCall{
73+
{
74+
expectedMethod: "NodeGetVolumeStats",
75+
expectedError: codes.OK,
76+
},
77+
},
78+
nodeVolumeConditionRequired: false,
79+
nodeAbnormalVolumeCondition: false,
80+
},
81+
{
82+
name: "return normal volume stats with abnormal volume condition",
83+
expectedCalls: []csiCall{
84+
{
85+
expectedMethod: "NodeGetVolumeStats",
86+
expectedError: codes.OK,
87+
},
88+
},
89+
nodeVolumeConditionRequired: true,
90+
nodeAbnormalVolumeCondition: true,
91+
},
92+
}
93+
for _, t := range tests {
94+
test := t
95+
ginkgo.It(test.name, func(ctx context.Context) {
96+
m.init(ctx, testParameters{
97+
registerDriver: true,
98+
enableNodeVolumeCondition: test.nodeVolumeConditionRequired,
99+
hooks: createGetVolumeStatsHook(test.nodeAbnormalVolumeCondition),
100+
})
101+
ginkgo.DeferCleanup(m.cleanup)
102+
_, claim, pod := m.createPod(ctx, pvcReference)
103+
if pod == nil {
104+
return
105+
}
106+
// Wait for PVC to get bound to make sure the CSI driver is fully started.
107+
err := e2epv.WaitForPersistentVolumeClaimPhase(ctx, v1.ClaimBound, f.ClientSet, f.Namespace.Name, claim.Name, time.Second, framework.ClaimProvisionTimeout)
108+
framework.ExpectNoError(err, "while waiting for PVC to get provisioned")
109+
110+
ginkgo.By("Waiting for pod to be running")
111+
err = e2epod.WaitForPodNameRunningInNamespace(ctx, m.cs, pod.Name, pod.Namespace)
112+
framework.ExpectNoError(err, "wait for running pod")
113+
ginkgo.By("Waiting for all remaining expected CSI calls")
114+
err = wait.PollUntilContextTimeout(ctx, time.Second, csiNodeVolumeStatWaitPeriod, true, func(c context.Context) (done bool, err error) {
115+
var index int
116+
_, index, err = compareCSICalls(ctx, trackedCalls, test.expectedCalls, m.driver.GetCalls)
117+
if err != nil {
118+
return true, err
119+
}
120+
if index == 0 {
121+
// No CSI call received yet
122+
return false, nil
123+
}
124+
if len(test.expectedCalls) == index {
125+
// all calls received
126+
return true, nil
127+
}
128+
return false, nil
129+
})
130+
framework.ExpectNoError(err, "while waiting for all CSI calls")
131+
// try to use ```csi.NewMetricsCsi(pv.handler).GetMetrics()``` to get metrics from csimock driver but failed.
132+
// the mocked csidriver register doesn't regist itself to normal csidriver.
133+
if test.nodeVolumeConditionRequired {
134+
pod, err := f.ClientSet.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
135+
framework.ExpectNoError(err, "get running pod")
136+
grabber, err := e2emetrics.NewMetricsGrabber(ctx, f.ClientSet, nil, f.ClientConfig(), true, false, false, false, false, false)
137+
framework.ExpectNoError(err, "creating the metrics grabber")
138+
waitErr := wait.PollUntilContextTimeout(ctx, 30*time.Second, csiNodeVolumeStatWaitPeriod, true, func(ctx context.Context) (bool, error) {
139+
framework.Logf("Grabbing Kubelet metrics")
140+
// Grab kubelet metrics from the node the pod was scheduled on
141+
var err error
142+
kubeMetrics, err := grabber.GrabFromKubelet(ctx, pod.Spec.NodeName)
143+
if err != nil {
144+
framework.Logf("Error fetching kubelet metrics err: %v", err)
145+
return false, err
146+
}
147+
if !findVolumeConditionMetrics(f.Namespace.Name, claim.Name, kubeMetrics, test.nodeAbnormalVolumeCondition) {
148+
return false, nil
149+
}
150+
return true, nil
151+
})
152+
framework.ExpectNoError(waitErr, "call metrics should not have any error")
153+
}
154+
})
155+
}
156+
157+
})
158+
})
159+
160+
func findVolumeConditionMetrics(pvcNamespace, pvcName string, kubeMetrics e2emetrics.KubeletMetrics, nodeAbnormalVolumeCondition bool) bool {
161+
162+
found := false
163+
framework.Logf("Looking for sample tagged with namespace `%s`, PVC `%s`", pvcNamespace, pvcName)
164+
for key, value := range kubeMetrics {
165+
for _, sample := range value {
166+
framework.Logf("Found sample %++v with key: %s", sample, key)
167+
samplePVC, ok := sample.Metric["persistentvolumeclaim"]
168+
if !ok {
169+
break
170+
}
171+
sampleNS, ok := sample.Metric["namespace"]
172+
if !ok {
173+
break
174+
}
175+
176+
if string(samplePVC) == pvcName && string(sampleNS) == pvcNamespace && strings.Contains(key, kubeletmetrics.VolumeStatsHealthStatusAbnormalKey) {
177+
if (nodeAbnormalVolumeCondition && sample.Value.String() == "1") || (!nodeAbnormalVolumeCondition && sample.Value.String() == "0") {
178+
found = true
179+
break
180+
}
181+
}
182+
}
183+
}
184+
return found
185+
}
186+
187+
func createGetVolumeStatsHook(abnormalVolumeCondition bool) *drivers.Hooks {
188+
return &drivers.Hooks{
189+
Pre: func(ctx context.Context, fullMethod string, request interface{}) (reply interface{}, err error) {
190+
if req, ok := request.(*csipbv1.NodeGetVolumeStatsRequest); ok {
191+
if abnormalVolumeCondition {
192+
req.VolumePath = "/tmp/csi/health/abnormal"
193+
}
194+
}
195+
return nil, nil
196+
},
197+
}
198+
199+
}

test/e2e/storage/drivers/csi-test/mock/service/node.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -359,19 +359,22 @@ func (s *service) NodeGetCapabilities(
359359
},
360360
},
361361
},
362-
{
362+
}
363+
if s.config.NodeExpansionRequired {
364+
capabilities = append(capabilities, &csi.NodeServiceCapability{
363365
Type: &csi.NodeServiceCapability_Rpc{
364366
Rpc: &csi.NodeServiceCapability_RPC{
365-
Type: csi.NodeServiceCapability_RPC_VOLUME_CONDITION,
367+
Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
366368
},
367369
},
368-
},
370+
})
369371
}
370-
if s.config.NodeExpansionRequired {
372+
373+
if s.config.NodeVolumeConditionRequired {
371374
capabilities = append(capabilities, &csi.NodeServiceCapability{
372375
Type: &csi.NodeServiceCapability_Rpc{
373376
Rpc: &csi.NodeServiceCapability_RPC{
374-
Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
377+
Type: csi.NodeServiceCapability_RPC_VOLUME_CONDITION,
375378
},
376379
},
377380
})
@@ -417,7 +420,10 @@ func (s *service) NodeGetVolumeStats(ctx context.Context,
417420
req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
418421

419422
resp := &csi.NodeGetVolumeStatsResponse{
420-
VolumeCondition: &csi.VolumeCondition{},
423+
VolumeCondition: &csi.VolumeCondition{
424+
Abnormal: false,
425+
Message: "volume is normal",
426+
},
421427
}
422428

423429
if len(req.GetVolumeId()) == 0 {
@@ -437,6 +443,19 @@ func (s *service) NodeGetVolumeStats(ctx context.Context,
437443

438444
nodeMntPathKey := path.Join(s.nodeID, req.VolumePath)
439445

446+
resp.Usage = []*csi.VolumeUsage{
447+
{
448+
Total: v.GetCapacityBytes(),
449+
Unit: csi.VolumeUsage_BYTES,
450+
},
451+
}
452+
453+
if req.GetVolumePath() == "/tmp/csi/health/abnormal" {
454+
resp.VolumeCondition.Abnormal = true
455+
resp.VolumeCondition.Message = "volume is abnormal"
456+
return resp, nil
457+
}
458+
440459
_, exists := v.VolumeContext[nodeMntPathKey]
441460
if !exists {
442461
msg := fmt.Sprintf("volume %q doest not exist on the specified path %q", req.VolumeId, req.VolumePath)
@@ -449,12 +468,5 @@ func (s *service) NodeGetVolumeStats(ctx context.Context,
449468
return nil, status.Errorf(hookVal, hookMsg)
450469
}
451470

452-
resp.Usage = []*csi.VolumeUsage{
453-
{
454-
Total: v.GetCapacityBytes(),
455-
Unit: csi.VolumeUsage_BYTES,
456-
},
457-
}
458-
459471
return resp, nil
460472
}

test/e2e/storage/drivers/csi-test/mock/service/service.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ var Manifest = map[string]string{
5151
}
5252

5353
type Config struct {
54-
DisableAttach bool
55-
DriverName string
56-
AttachLimit int64
57-
NodeExpansionRequired bool
58-
VolumeMountGroupRequired bool
59-
DisableControllerExpansion bool
60-
DisableOnlineExpansion bool
61-
PermissiveTargetPath bool
62-
EnableTopology bool
63-
IO DirIO
54+
DisableAttach bool
55+
DriverName string
56+
AttachLimit int64
57+
NodeExpansionRequired bool
58+
NodeVolumeConditionRequired bool
59+
VolumeMountGroupRequired bool
60+
DisableControllerExpansion bool
61+
DisableOnlineExpansion bool
62+
PermissiveTargetPath bool
63+
EnableTopology bool
64+
IO DirIO
6465
}
6566

6667
// DirIO is an abstraction over direct os calls.

test/e2e/storage/drivers/csi.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ type mockCSIDriver struct {
344344
requiresRepublish *bool
345345
fsGroupPolicy *storagev1.FSGroupPolicy
346346
enableVolumeMountGroup bool
347+
enableNodeVolumeCondition bool
347348
embedded bool
348349
calls MockCSICalls
349350
embeddedCSIDriver *mockdriver.CSIDriver
@@ -393,6 +394,7 @@ type CSIMockDriverOpts struct {
393394
EnableNodeExpansion bool
394395
EnableSnapshot bool
395396
EnableVolumeMountGroup bool
397+
EnableNodeVolumeCondition bool
396398
TokenRequests []storagev1.TokenRequest
397399
RequiresRepublish *bool
398400
FSGroupPolicy *storagev1.FSGroupPolicy
@@ -547,6 +549,7 @@ func InitMockCSIDriver(driverOpts CSIMockDriverOpts) MockCSITestDriver {
547549
attachable: !driverOpts.DisableAttach,
548550
attachLimit: driverOpts.AttachLimit,
549551
enableNodeExpansion: driverOpts.EnableNodeExpansion,
552+
enableNodeVolumeCondition: driverOpts.EnableNodeVolumeCondition,
550553
tokenRequests: driverOpts.TokenRequests,
551554
requiresRepublish: driverOpts.RequiresRepublish,
552555
fsGroupPolicy: driverOpts.FSGroupPolicy,
@@ -621,12 +624,13 @@ func (m *mockCSIDriver) PrepareTest(ctx context.Context, f *framework.Framework)
621624
// for cleanup callbacks.
622625
ctx, cancel := context.WithCancel(context.Background())
623626
serviceConfig := mockservice.Config{
624-
DisableAttach: !m.attachable,
625-
DriverName: "csi-mock-" + f.UniqueName,
626-
AttachLimit: int64(m.attachLimit),
627-
NodeExpansionRequired: m.enableNodeExpansion,
628-
VolumeMountGroupRequired: m.enableVolumeMountGroup,
629-
EnableTopology: m.enableTopology,
627+
DisableAttach: !m.attachable,
628+
DriverName: "csi-mock-" + f.UniqueName,
629+
AttachLimit: int64(m.attachLimit),
630+
NodeExpansionRequired: m.enableNodeExpansion,
631+
NodeVolumeConditionRequired: m.enableNodeVolumeCondition,
632+
VolumeMountGroupRequired: m.enableVolumeMountGroup,
633+
EnableTopology: m.enableTopology,
630634
IO: proxy.PodDirIO{
631635
F: f,
632636
Namespace: m.driverNamespace.Name,

0 commit comments

Comments
 (0)