1
16
17 package testsuites
18
19 import (
20 "context"
21 "fmt"
22 "strings"
23 "time"
24
25 "github.com/onsi/ginkgo/v2"
26 "github.com/onsi/gomega"
27 "github.com/onsi/gomega/types"
28
29 storagev1 "k8s.io/api/storage/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/client-go/kubernetes"
32 "k8s.io/kubernetes/test/e2e/framework"
33 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
34 e2evolume "k8s.io/kubernetes/test/e2e/framework/volume"
35 storageframework "k8s.io/kubernetes/test/e2e/storage/framework"
36 admissionapi "k8s.io/pod-security-admission/api"
37 )
38
39 type capacityTestSuite struct {
40 tsInfo storageframework.TestSuiteInfo
41 }
42
43
44
45 func InitCustomCapacityTestSuite(patterns []storageframework.TestPattern) storageframework.TestSuite {
46 return &capacityTestSuite{
47 tsInfo: storageframework.TestSuiteInfo{
48 Name: "capacity",
49 TestPatterns: patterns,
50 SupportedSizeRange: e2evolume.SizeRange{
51 Min: "1Mi",
52 },
53 },
54 }
55 }
56
57
58
59 func InitCapacityTestSuite() storageframework.TestSuite {
60 patterns := []storageframework.TestPattern{
61 storageframework.DefaultFsDynamicPV,
62 }
63 return InitCustomCapacityTestSuite(patterns)
64 }
65
66 func (p *capacityTestSuite) GetTestSuiteInfo() storageframework.TestSuiteInfo {
67 return p.tsInfo
68 }
69
70 func (p *capacityTestSuite) SkipUnsupportedTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
71
72 if pattern.VolType != storageframework.DynamicPV {
73 e2eskipper.Skipf("Suite %q does not support %v", p.tsInfo.Name, pattern.VolType)
74 }
75 dInfo := driver.GetDriverInfo()
76 if !dInfo.Capabilities[storageframework.CapCapacity] {
77 e2eskipper.Skipf("Driver %s doesn't publish storage capacity -- skipping", dInfo.Name)
78 }
79 }
80
81 func (p *capacityTestSuite) DefineTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
82 var (
83 dInfo = driver.GetDriverInfo()
84 dDriver storageframework.DynamicPVTestDriver
85 sc *storagev1.StorageClass
86 )
87
88
89
90 f := framework.NewFrameworkWithCustomTimeouts("capacity", storageframework.GetDriverTimeouts(driver))
91 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
92
93 init := func(ctx context.Context) {
94 dDriver, _ = driver.(storageframework.DynamicPVTestDriver)
95
96 config := driver.PrepareTest(ctx, f)
97 sc = dDriver.GetDynamicProvisionStorageClass(ctx, config, pattern.FsType)
98 if sc == nil {
99 e2eskipper.Skipf("Driver %q does not define Dynamic Provision StorageClass - skipping", dInfo.Name)
100 }
101 }
102
103 ginkgo.It("provides storage capacity information", func(ctx context.Context) {
104 init(ctx)
105
106 timeout := time.Minute
107 pollInterval := time.Second
108 matchSC := HaveCapacitiesForClass(sc.Name)
109 listAll := gomega.Eventually(ctx, func() (*storagev1.CSIStorageCapacityList, error) {
110 return f.ClientSet.StorageV1().CSIStorageCapacities("").List(ctx, metav1.ListOptions{})
111 }, timeout, pollInterval)
112
113
114
115
116
117
118 matcher := matchSC
119 if len(dInfo.TopologyKeys) == 1 {
120
121
122
123
124
125
126
127
128
129
130 matcher = HaveCapacitiesForClassAndNodes(ctx, f.ClientSet, sc.Provisioner, sc.Name, dInfo.TopologyKeys[0])
131 }
132
133
134 sc := SetupStorageClass(ctx, f.ClientSet, sc)
135 listAll.Should(MatchCapacities(matcher), "after creating storage class")
136
137
138 err := f.ClientSet.StorageV1().StorageClasses().Delete(ctx, sc.Name, metav1.DeleteOptions{})
139 framework.ExpectNoError(err, "delete storage class")
140 listAll.ShouldNot(MatchCapacities(matchSC), "after deleting storage class")
141 })
142 }
143
144 func formatCapacities(capacities []storagev1.CSIStorageCapacity) []string {
145 lines := []string{}
146 for _, capacity := range capacities {
147 lines = append(lines, fmt.Sprintf(" %+v", capacity))
148 }
149 return lines
150 }
151
152
153
154 func MatchCapacities(match types.GomegaMatcher) types.GomegaMatcher {
155 return matchCSIStorageCapacities{match: match}
156 }
157
158 type matchCSIStorageCapacities struct {
159 match types.GomegaMatcher
160 }
161
162 var _ types.GomegaMatcher = matchCSIStorageCapacities{}
163
164 func (m matchCSIStorageCapacities) Match(actual interface{}) (success bool, err error) {
165 return m.match.Match(actual)
166 }
167
168 func (m matchCSIStorageCapacities) FailureMessage(actual interface{}) (message string) {
169 return m.match.FailureMessage(actual) + m.dump(actual)
170 }
171
172 func (m matchCSIStorageCapacities) NegatedFailureMessage(actual interface{}) (message string) {
173 return m.match.NegatedFailureMessage(actual) + m.dump(actual)
174 }
175
176 func (m matchCSIStorageCapacities) dump(actual interface{}) string {
177 capacities, ok := actual.(*storagev1.CSIStorageCapacityList)
178 if !ok || capacities == nil {
179 return ""
180 }
181 lines := []string{"\n\nall CSIStorageCapacity objects:"}
182 for _, capacity := range capacities.Items {
183 lines = append(lines, fmt.Sprintf("%+v", capacity))
184 }
185 return strings.Join(lines, "\n")
186 }
187
188
189
190
191 type CapacityMatcher interface {
192 types.GomegaMatcher
193
194
195 MatchedCapacities() []storagev1.CSIStorageCapacity
196 }
197
198
199
200 func HaveCapacitiesForClass(scName string) CapacityMatcher {
201 return &haveCSIStorageCapacities{scName: scName}
202 }
203
204 type haveCSIStorageCapacities struct {
205 scName string
206 matchingCapacities []storagev1.CSIStorageCapacity
207 }
208
209 var _ CapacityMatcher = &haveCSIStorageCapacities{}
210
211 func (h *haveCSIStorageCapacities) Match(actual interface{}) (success bool, err error) {
212 capacities, ok := actual.(*storagev1.CSIStorageCapacityList)
213 if !ok {
214 return false, fmt.Errorf("expected *storagev1.CSIStorageCapacityList, got: %T", actual)
215 }
216 h.matchingCapacities = nil
217 for _, capacity := range capacities.Items {
218 if capacity.StorageClassName == h.scName {
219 h.matchingCapacities = append(h.matchingCapacities, capacity)
220 }
221 }
222 return len(h.matchingCapacities) > 0, nil
223 }
224
225 func (h *haveCSIStorageCapacities) MatchedCapacities() []storagev1.CSIStorageCapacity {
226 return h.matchingCapacities
227 }
228
229 func (h *haveCSIStorageCapacities) FailureMessage(actual interface{}) (message string) {
230 return fmt.Sprintf("no CSIStorageCapacity objects for storage class %q", h.scName)
231 }
232
233 func (h *haveCSIStorageCapacities) NegatedFailureMessage(actual interface{}) (message string) {
234 return fmt.Sprintf("CSIStorageCapacity objects for storage class %q:\n%s",
235 h.scName,
236 strings.Join(formatCapacities(h.matchingCapacities), "\n"),
237 )
238 }
239
240
241
242 func HaveCapacitiesForClassAndNodes(ctx context.Context, client kubernetes.Interface, driverName, scName, topologyKey string) CapacityMatcher {
243 return &haveLocalStorageCapacities{
244 ctx: ctx,
245 client: client,
246 driverName: driverName,
247 match: HaveCapacitiesForClass(scName),
248 topologyKey: topologyKey,
249 }
250 }
251
252 type haveLocalStorageCapacities struct {
253 ctx context.Context
254 client kubernetes.Interface
255 driverName string
256 match CapacityMatcher
257 topologyKey string
258
259 matchSuccess bool
260 expectedCapacities []storagev1.CSIStorageCapacity
261 unexpectedCapacities []storagev1.CSIStorageCapacity
262 missingTopologyValues []string
263 }
264
265 var _ CapacityMatcher = &haveLocalStorageCapacities{}
266
267 func (h *haveLocalStorageCapacities) Match(actual interface{}) (success bool, err error) {
268 ctx := h.ctx
269 h.expectedCapacities = nil
270 h.unexpectedCapacities = nil
271 h.missingTopologyValues = nil
272
273
274 success, err = h.match.Match(actual)
275 h.matchSuccess = success
276 if !success || err != nil {
277 return
278 }
279
280
281 csiNodes, err := h.client.StorageV1().CSINodes().List(ctx, metav1.ListOptions{})
282 if err != nil {
283 return false, err
284 }
285 topologyValues := map[string]bool{}
286 for _, csiNode := range csiNodes.Items {
287 for _, driver := range csiNode.Spec.Drivers {
288 if driver.Name != h.driverName {
289 continue
290 }
291 node, err := h.client.CoreV1().Nodes().Get(ctx, csiNode.Name, metav1.GetOptions{})
292 if err != nil {
293 return false, err
294 }
295 value, ok := node.Labels[h.topologyKey]
296 if !ok || value == "" {
297 return false, fmt.Errorf("driver %q should run on node %q, but its topology label %q was not set",
298 h.driverName,
299 node.Name,
300 h.topologyKey)
301 }
302 topologyValues[value] = true
303 break
304 }
305 }
306 if len(topologyValues) == 0 {
307 return false, fmt.Errorf("driver %q not running on any node", h.driverName)
308 }
309
310
311 remainingTopologyValues := map[string]bool{}
312 for value := range topologyValues {
313 remainingTopologyValues[value] = true
314 }
315 capacities := h.match.MatchedCapacities()
316 for _, capacity := range capacities {
317 if capacity.NodeTopology == nil ||
318 len(capacity.NodeTopology.MatchExpressions) > 0 ||
319 len(capacity.NodeTopology.MatchLabels) != 1 ||
320 !remainingTopologyValues[capacity.NodeTopology.MatchLabels[h.topologyKey]] {
321 h.unexpectedCapacities = append(h.unexpectedCapacities, capacity)
322 continue
323 }
324 remainingTopologyValues[capacity.NodeTopology.MatchLabels[h.topologyKey]] = false
325 h.expectedCapacities = append(h.expectedCapacities, capacity)
326 }
327
328
329 for value, remaining := range remainingTopologyValues {
330 if remaining {
331 h.missingTopologyValues = append(h.missingTopologyValues, value)
332 }
333 }
334 return len(h.unexpectedCapacities) == 0 && len(h.missingTopologyValues) == 0, nil
335 }
336
337 func (h *haveLocalStorageCapacities) MatchedCapacities() []storagev1.CSIStorageCapacity {
338 return h.match.MatchedCapacities()
339 }
340
341 func (h *haveLocalStorageCapacities) FailureMessage(actual interface{}) (message string) {
342 if !h.matchSuccess {
343 return h.match.FailureMessage(actual)
344 }
345 var lines []string
346 if len(h.unexpectedCapacities) != 0 {
347 lines = append(lines, "unexpected CSIStorageCapacity objects:")
348 lines = append(lines, formatCapacities(h.unexpectedCapacities)...)
349 }
350 if len(h.missingTopologyValues) != 0 {
351 lines = append(lines, fmt.Sprintf("no CSIStorageCapacity objects with topology key %q and values %v",
352 h.topologyKey, h.missingTopologyValues,
353 ))
354 }
355 return strings.Join(lines, "\n")
356 }
357
358 func (h *haveLocalStorageCapacities) NegatedFailureMessage(actual interface{}) (message string) {
359 if h.matchSuccess {
360 return h.match.NegatedFailureMessage(actual)
361 }
362
363 var lines []string
364 if len(h.expectedCapacities) != 0 {
365 lines = append(lines, "expected CSIStorageCapacity objects:")
366 lines = append(lines, formatCapacities(h.expectedCapacities)...)
367 }
368 if len(h.unexpectedCapacities) != 0 {
369 lines = append(lines, "unexpected CSIStorageCapacity objects:")
370 lines = append(lines, formatCapacities(h.unexpectedCapacities)...)
371 }
372 if len(h.missingTopologyValues) != 0 {
373 lines = append(lines, fmt.Sprintf("no CSIStorageCapacity objects with topology key %q and values %v",
374 h.topologyKey, h.missingTopologyValues,
375 ))
376 }
377 return strings.Join(lines, "\n")
378 }
379
View as plain text