1
16
17 package label
18
19 import (
20 "context"
21 "errors"
22 "reflect"
23 "sort"
24 "testing"
25
26 v1 "k8s.io/api/core/v1"
27 apierrors "k8s.io/apimachinery/pkg/api/errors"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/runtime/schema"
30 "k8s.io/apiserver/pkg/admission"
31 admissiontesting "k8s.io/apiserver/pkg/admission/testing"
32 cloudprovider "k8s.io/cloud-provider"
33 persistentvolume "k8s.io/component-helpers/storage/volume"
34 api "k8s.io/kubernetes/pkg/apis/core"
35 )
36
37 type mockVolumes struct {
38 volumeLabels map[string]string
39 volumeLabelsError error
40 }
41
42 var _ cloudprovider.PVLabeler = &mockVolumes{}
43
44 func (v *mockVolumes) GetLabelsForVolume(ctx context.Context, pv *v1.PersistentVolume) (map[string]string, error) {
45 return v.volumeLabels, v.volumeLabelsError
46 }
47
48 func mockVolumeFailure(err error) *mockVolumes {
49 return &mockVolumes{volumeLabelsError: err}
50 }
51
52 func mockVolumeLabels(labels map[string]string) *mockVolumes {
53 return &mockVolumes{volumeLabels: labels}
54 }
55
56 func Test_PVLAdmission(t *testing.T) {
57 testcases := []struct {
58 name string
59 handler *persistentVolumeLabel
60 pvlabeler cloudprovider.PVLabeler
61 preAdmissionPV *api.PersistentVolume
62 postAdmissionPV *api.PersistentVolume
63 err error
64 }{
65 {
66 name: "non-cloud PV ignored",
67 handler: newPersistentVolumeLabel(),
68 pvlabeler: mockVolumeLabels(map[string]string{
69 "a": "1",
70 "b": "2",
71 v1.LabelTopologyZone: "1__2__3",
72 }),
73 preAdmissionPV: &api.PersistentVolume{
74 ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
75 Spec: api.PersistentVolumeSpec{
76 PersistentVolumeSource: api.PersistentVolumeSource{
77 HostPath: &api.HostPathVolumeSource{
78 Path: "/",
79 },
80 },
81 },
82 },
83 postAdmissionPV: &api.PersistentVolume{
84 ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
85 Spec: api.PersistentVolumeSpec{
86 PersistentVolumeSource: api.PersistentVolumeSource{
87 HostPath: &api.HostPathVolumeSource{
88 Path: "/",
89 },
90 },
91 },
92 },
93 err: nil,
94 },
95 {
96 name: "cloud provider error blocks creation of volume",
97 handler: newPersistentVolumeLabel(),
98 pvlabeler: mockVolumeFailure(errors.New("invalid volume")),
99 preAdmissionPV: &api.PersistentVolume{
100 ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
101 Spec: api.PersistentVolumeSpec{
102 PersistentVolumeSource: api.PersistentVolumeSource{
103 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
104 PDName: "123",
105 },
106 },
107 },
108 },
109 postAdmissionPV: &api.PersistentVolume{
110 ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
111 Spec: api.PersistentVolumeSpec{
112 PersistentVolumeSource: api.PersistentVolumeSource{
113 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
114 PDName: "123",
115 },
116 },
117 },
118 },
119 err: apierrors.NewForbidden(schema.ParseGroupResource("persistentvolumes"), "gcepd", errors.New("error querying GCE PD volume 123: invalid volume")),
120 },
121 {
122 name: "cloud provider returns no labels",
123 handler: newPersistentVolumeLabel(),
124 pvlabeler: mockVolumeLabels(map[string]string{}),
125 preAdmissionPV: &api.PersistentVolume{
126 ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
127 Spec: api.PersistentVolumeSpec{
128 PersistentVolumeSource: api.PersistentVolumeSource{
129 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
130 VolumeID: "123",
131 },
132 },
133 },
134 },
135 postAdmissionPV: &api.PersistentVolume{
136 ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
137 Spec: api.PersistentVolumeSpec{
138 PersistentVolumeSource: api.PersistentVolumeSource{
139 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
140 VolumeID: "123",
141 },
142 },
143 },
144 },
145 err: nil,
146 },
147 {
148 name: "cloud provider returns nil, nil",
149 handler: newPersistentVolumeLabel(),
150 pvlabeler: mockVolumeFailure(nil),
151 preAdmissionPV: &api.PersistentVolume{
152 ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
153 Spec: api.PersistentVolumeSpec{
154 PersistentVolumeSource: api.PersistentVolumeSource{
155 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
156 VolumeID: "123",
157 },
158 },
159 },
160 },
161 postAdmissionPV: &api.PersistentVolume{
162 ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
163 Spec: api.PersistentVolumeSpec{
164 PersistentVolumeSource: api.PersistentVolumeSource{
165 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
166 VolumeID: "123",
167 },
168 },
169 },
170 },
171 err: nil,
172 },
173 {
174 name: "existing Beta labels from dynamic provisioning are not changed",
175 handler: newPersistentVolumeLabel(),
176 pvlabeler: mockVolumeLabels(map[string]string{
177 v1.LabelFailureDomainBetaZone: "domain1",
178 v1.LabelFailureDomainBetaRegion: "region1",
179 }),
180 preAdmissionPV: &api.PersistentVolume{
181 ObjectMeta: metav1.ObjectMeta{
182 Name: "awsebs", Namespace: "myns",
183 Labels: map[string]string{
184 v1.LabelFailureDomainBetaZone: "existingDomain",
185 v1.LabelFailureDomainBetaRegion: "existingRegion",
186 },
187 Annotations: map[string]string{
188 persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
189 },
190 },
191 Spec: api.PersistentVolumeSpec{
192 PersistentVolumeSource: api.PersistentVolumeSource{
193 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
194 VolumeID: "123",
195 },
196 },
197 },
198 },
199 postAdmissionPV: &api.PersistentVolume{
200 ObjectMeta: metav1.ObjectMeta{
201 Name: "awsebs",
202 Namespace: "myns",
203 Labels: map[string]string{
204 v1.LabelFailureDomainBetaZone: "existingDomain",
205 v1.LabelFailureDomainBetaRegion: "existingRegion",
206 },
207 Annotations: map[string]string{
208 persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
209 },
210 },
211 Spec: api.PersistentVolumeSpec{
212 PersistentVolumeSource: api.PersistentVolumeSource{
213 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
214 VolumeID: "123",
215 },
216 },
217 NodeAffinity: &api.VolumeNodeAffinity{
218 Required: &api.NodeSelector{
219 NodeSelectorTerms: []api.NodeSelectorTerm{
220 {
221 MatchExpressions: []api.NodeSelectorRequirement{
222 {
223 Key: v1.LabelFailureDomainBetaRegion,
224 Operator: api.NodeSelectorOpIn,
225 Values: []string{"existingRegion"},
226 },
227 {
228 Key: v1.LabelFailureDomainBetaZone,
229 Operator: api.NodeSelectorOpIn,
230 Values: []string{"existingDomain"},
231 },
232 },
233 },
234 },
235 },
236 },
237 },
238 },
239 err: nil,
240 },
241 {
242 name: "existing GA labels from dynamic provisioning are not changed",
243 handler: newPersistentVolumeLabel(),
244 pvlabeler: mockVolumeLabels(map[string]string{
245 v1.LabelTopologyZone: "domain1",
246 v1.LabelTopologyRegion: "region1",
247 }),
248 preAdmissionPV: &api.PersistentVolume{
249 ObjectMeta: metav1.ObjectMeta{
250 Name: "awsebs", Namespace: "myns",
251 Labels: map[string]string{
252 v1.LabelTopologyZone: "existingDomain",
253 v1.LabelTopologyRegion: "existingRegion",
254 },
255 Annotations: map[string]string{
256 persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
257 },
258 },
259 Spec: api.PersistentVolumeSpec{
260 PersistentVolumeSource: api.PersistentVolumeSource{
261 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
262 VolumeID: "123",
263 },
264 },
265 },
266 },
267 postAdmissionPV: &api.PersistentVolume{
268 ObjectMeta: metav1.ObjectMeta{
269 Name: "awsebs",
270 Namespace: "myns",
271 Labels: map[string]string{
272 v1.LabelTopologyZone: "existingDomain",
273 v1.LabelTopologyRegion: "existingRegion",
274 },
275 Annotations: map[string]string{
276 persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
277 },
278 },
279 Spec: api.PersistentVolumeSpec{
280 PersistentVolumeSource: api.PersistentVolumeSource{
281 AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
282 VolumeID: "123",
283 },
284 },
285 NodeAffinity: &api.VolumeNodeAffinity{
286 Required: &api.NodeSelector{
287 NodeSelectorTerms: []api.NodeSelectorTerm{
288 {
289 MatchExpressions: []api.NodeSelectorRequirement{
290 {
291 Key: v1.LabelTopologyRegion,
292 Operator: api.NodeSelectorOpIn,
293 Values: []string{"existingRegion"},
294 },
295 {
296 Key: v1.LabelTopologyZone,
297 Operator: api.NodeSelectorOpIn,
298 Values: []string{"existingDomain"},
299 },
300 },
301 },
302 },
303 },
304 },
305 },
306 },
307 err: nil,
308 },
309 {
310 name: "existing labels from user are changed",
311 handler: newPersistentVolumeLabel(),
312 pvlabeler: mockVolumeLabels(map[string]string{
313 v1.LabelTopologyZone: "domain1",
314 v1.LabelTopologyRegion: "region1",
315 }),
316 preAdmissionPV: &api.PersistentVolume{
317 ObjectMeta: metav1.ObjectMeta{
318 Name: "gcePV", Namespace: "myns",
319 Labels: map[string]string{
320 v1.LabelTopologyZone: "existingDomain",
321 v1.LabelTopologyRegion: "existingRegion",
322 },
323 },
324 Spec: api.PersistentVolumeSpec{
325 PersistentVolumeSource: api.PersistentVolumeSource{
326 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
327 PDName: "123",
328 },
329 },
330 },
331 },
332 postAdmissionPV: &api.PersistentVolume{
333 ObjectMeta: metav1.ObjectMeta{
334 Name: "gcePV",
335 Namespace: "myns",
336 Labels: map[string]string{
337 v1.LabelTopologyZone: "domain1",
338 v1.LabelTopologyRegion: "region1",
339 },
340 },
341 Spec: api.PersistentVolumeSpec{
342 PersistentVolumeSource: api.PersistentVolumeSource{
343 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
344 PDName: "123",
345 },
346 },
347 NodeAffinity: &api.VolumeNodeAffinity{
348 Required: &api.NodeSelector{
349 NodeSelectorTerms: []api.NodeSelectorTerm{
350 {
351 MatchExpressions: []api.NodeSelectorRequirement{
352 {
353 Key: v1.LabelTopologyRegion,
354 Operator: api.NodeSelectorOpIn,
355 Values: []string{"region1"},
356 },
357 {
358 Key: v1.LabelTopologyZone,
359 Operator: api.NodeSelectorOpIn,
360 Values: []string{"domain1"},
361 },
362 },
363 },
364 },
365 },
366 },
367 },
368 },
369 err: nil,
370 },
371 {
372 name: "GCE PD PV labeled correctly",
373 handler: newPersistentVolumeLabel(),
374 pvlabeler: mockVolumeLabels(map[string]string{
375 "a": "1",
376 "b": "2",
377 v1.LabelTopologyZone: "1__2__3",
378 }),
379 preAdmissionPV: &api.PersistentVolume{
380 ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
381 Spec: api.PersistentVolumeSpec{
382 PersistentVolumeSource: api.PersistentVolumeSource{
383 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
384 PDName: "123",
385 },
386 },
387 },
388 },
389 postAdmissionPV: &api.PersistentVolume{
390 ObjectMeta: metav1.ObjectMeta{
391 Name: "gcepd",
392 Namespace: "myns",
393 Labels: map[string]string{
394 "a": "1",
395 "b": "2",
396 v1.LabelTopologyZone: "1__2__3",
397 },
398 },
399 Spec: api.PersistentVolumeSpec{
400 PersistentVolumeSource: api.PersistentVolumeSource{
401 GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
402 PDName: "123",
403 },
404 },
405 NodeAffinity: &api.VolumeNodeAffinity{
406 Required: &api.NodeSelector{
407 NodeSelectorTerms: []api.NodeSelectorTerm{
408 {
409 MatchExpressions: []api.NodeSelectorRequirement{
410 {
411 Key: "a",
412 Operator: api.NodeSelectorOpIn,
413 Values: []string{"1"},
414 },
415 {
416 Key: "b",
417 Operator: api.NodeSelectorOpIn,
418 Values: []string{"2"},
419 },
420 {
421 Key: v1.LabelTopologyZone,
422 Operator: api.NodeSelectorOpIn,
423 Values: []string{"1", "2", "3"},
424 },
425 },
426 },
427 },
428 },
429 },
430 },
431 },
432 err: nil,
433 },
434 }
435
436 for _, testcase := range testcases {
437 t.Run(testcase.name, func(t *testing.T) {
438 setPVLabeler(testcase.handler, testcase.pvlabeler)
439 handler := admissiontesting.WithReinvocationTesting(t, admission.NewChainHandler(testcase.handler))
440
441 err := handler.Admit(context.TODO(), admission.NewAttributesRecord(testcase.preAdmissionPV, nil, api.Kind("PersistentVolume").WithVersion("version"), testcase.preAdmissionPV.Namespace, testcase.preAdmissionPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
442 if !reflect.DeepEqual(err, testcase.err) {
443 t.Logf("expected error: %q", testcase.err)
444 t.Logf("actual error: %q", err)
445 t.Error("unexpected error when admitting PV")
446 }
447
448
449 sortMatchExpressions(testcase.preAdmissionPV)
450 if !reflect.DeepEqual(testcase.preAdmissionPV, testcase.postAdmissionPV) {
451 t.Logf("expected PV: %+v", testcase.postAdmissionPV)
452 t.Logf("actual PV: %+v", testcase.preAdmissionPV)
453 t.Error("unexpected PV")
454 }
455
456 })
457 }
458 }
459
460
461
462
463
464 func setPVLabeler(handler *persistentVolumeLabel, pvlabeler cloudprovider.PVLabeler) {
465 handler.gcePVLabeler = pvlabeler
466 }
467
468
469 func sortMatchExpressions(pv *api.PersistentVolume) {
470 if pv.Spec.NodeAffinity == nil ||
471 pv.Spec.NodeAffinity.Required == nil ||
472 pv.Spec.NodeAffinity.Required.NodeSelectorTerms == nil {
473 return
474 }
475
476 match := pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions
477 sort.Slice(match, func(i, j int) bool {
478 return match[i].Key < match[j].Key
479 })
480
481 pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = match
482 }
483
View as plain text