1 package storage
2
3 import (
4 "strings"
5 "testing"
6
7 "github.com/golang/mock/gomock"
8 "github.com/google/go-cmp/cmp"
9 appsv1 "k8s.io/api/apps/v1"
10 corev1 "k8s.io/api/core/v1"
11 "k8s.io/apimachinery/pkg/api/resource"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 "k8s.io/apimachinery/pkg/runtime"
14 ktesting "k8s.io/client-go/testing"
15
16 "github.com/redhat-developer/odo/pkg/kclient"
17 odolabels "github.com/redhat-developer/odo/pkg/labels"
18 "github.com/redhat-developer/odo/pkg/testingutil"
19 "github.com/redhat-developer/odo/pkg/util"
20 )
21
22 func Test_kubernetesClient_List(t *testing.T) {
23 type fields struct {
24 generic generic
25 }
26 tests := []struct {
27 name string
28 fields fields
29 returnedDeployments *appsv1.DeploymentList
30 returnedPVCs *corev1.PersistentVolumeClaimList
31 want StorageList
32 wantErr bool
33 }{
34 {
35 name: "case 1: should error out for multiple pods returned",
36 fields: fields{
37 generic: generic{
38 appName: "app",
39 componentName: "nodejs",
40 },
41 },
42 returnedDeployments: &appsv1.DeploymentList{
43 Items: []appsv1.Deployment{
44 *testingutil.CreateFakeDeployment("nodejs", true),
45 *testingutil.CreateFakeDeployment("nodejs", true),
46 },
47 },
48 wantErr: true,
49 },
50 {
51 name: "case 2: pod not found",
52 fields: fields{
53 generic: generic{
54 appName: "app",
55 componentName: "nodejs",
56 },
57 },
58 returnedDeployments: &appsv1.DeploymentList{
59 Items: []appsv1.Deployment{},
60 },
61 want: StorageList{},
62 wantErr: false,
63 },
64 {
65 name: "case 3: no volume mounts on pod",
66 fields: fields{
67 generic: generic{
68 componentName: "nodejs",
69 appName: "app",
70 },
71 },
72 returnedDeployments: &appsv1.DeploymentList{
73 Items: []appsv1.Deployment{
74 *testingutil.CreateFakeDeployment("nodejs", true),
75 },
76 },
77 want: StorageList{},
78 wantErr: false,
79 },
80 {
81 name: "case 4: two volumes mounted on a single container",
82 fields: fields{
83 generic: generic{
84 componentName: "nodejs",
85 appName: "app",
86 },
87 },
88 returnedDeployments: &appsv1.DeploymentList{
89 Items: []appsv1.Deployment{
90 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
91 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
92 {Name: "volume-0-vol", MountPath: "/data"},
93 {Name: "volume-1-vol", MountPath: "/path"},
94 }),
95 }, []corev1.Container{}, true),
96 },
97 },
98 returnedPVCs: &corev1.PersistentVolumeClaimList{
99 Items: []corev1.PersistentVolumeClaim{
100 *testingutil.FakePVC("volume-0", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
101 *testingutil.FakePVC("volume-1", "10Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-1").Labels()),
102 },
103 },
104 want: StorageList{
105 Items: []Storage{
106 generateStorage(NewStorage("volume-0", "5Gi", "/data", nil), "", "container-0"),
107 generateStorage(NewStorage("volume-1", "10Gi", "/path", nil), "", "container-0"),
108 },
109 },
110 wantErr: false,
111 },
112 {
113 name: "case 5: one volume is mounted on a single container and another on both",
114 fields: fields{
115 generic: generic{
116 appName: "app",
117 componentName: "nodejs",
118 },
119 },
120 returnedDeployments: &appsv1.DeploymentList{
121 Items: []appsv1.Deployment{
122 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
123 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
124 {Name: "volume-0-vol", MountPath: "/data"},
125 {Name: "volume-1-vol", MountPath: "/path"},
126 }),
127 testingutil.CreateFakeContainerWithVolumeMounts("container-1", []corev1.VolumeMount{
128 {Name: "volume-1-vol", MountPath: "/path"},
129 }),
130 }, []corev1.Container{}, true),
131 },
132 },
133 returnedPVCs: &corev1.PersistentVolumeClaimList{
134 Items: []corev1.PersistentVolumeClaim{
135 *testingutil.FakePVC("volume-0", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
136 *testingutil.FakePVC("volume-1", "10Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-1").Labels()),
137 },
138 },
139 want: StorageList{
140 Items: []Storage{
141 generateStorage(NewStorage("volume-0", "5Gi", "/data", nil), "", "container-0"),
142 generateStorage(NewStorage("volume-1", "10Gi", "/path", nil), "", "container-0"),
143 generateStorage(NewStorage("volume-1", "10Gi", "/path", nil), "", "container-1"),
144 },
145 },
146 wantErr: false,
147 },
148 {
149 name: "case 6: pvc for volumeMount not found",
150 fields: fields{
151 generic: generic{
152 componentName: "nodejs",
153 appName: "app",
154 },
155 },
156 returnedDeployments: &appsv1.DeploymentList{
157 Items: []appsv1.Deployment{
158 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
159 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
160 {Name: "volume-0-vol", MountPath: "/data"},
161 }),
162 testingutil.CreateFakeContainer("container-1"),
163 }, []corev1.Container{}, true),
164 },
165 },
166 returnedPVCs: &corev1.PersistentVolumeClaimList{
167 Items: []corev1.PersistentVolumeClaim{
168 *testingutil.FakePVC("volume-0", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
169 *testingutil.FakePVC("volume-1", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-1").Labels()),
170 },
171 },
172 wantErr: true,
173 },
174 {
175 name: "case 7: the storage label should be used as the name of the storage",
176 fields: fields{
177 generic: generic{
178 componentName: "nodejs",
179 appName: "app",
180 },
181 },
182 returnedDeployments: &appsv1.DeploymentList{
183 Items: []appsv1.Deployment{
184 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
185 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
186 {Name: "volume-0-nodejs-vol", MountPath: "/data"},
187 }),
188 }, []corev1.Container{}, true),
189 },
190 },
191 returnedPVCs: &corev1.PersistentVolumeClaimList{
192 Items: []corev1.PersistentVolumeClaim{
193 *testingutil.FakePVC("volume-0-nodejs", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
194 },
195 },
196 want: StorageList{
197 Items: []Storage{
198 generateStorage(NewStorage("volume-0", "5Gi", "/data", nil), "", "container-0"),
199 },
200 },
201 wantErr: false,
202 },
203 {
204 name: "case 8: no pvc found for mount path",
205 fields: fields{
206 generic: generic{
207 appName: "app",
208 componentName: "nodejs",
209 },
210 },
211 returnedDeployments: &appsv1.DeploymentList{
212 Items: []appsv1.Deployment{
213 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
214 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
215 {Name: "volume-0-vol", MountPath: "/data"},
216 {Name: "volume-1-vol", MountPath: "/path"},
217 }),
218 testingutil.CreateFakeContainerWithVolumeMounts("container-1", []corev1.VolumeMount{
219 {Name: "volume-1-vol", MountPath: "/path"},
220 {Name: "volume-vol", MountPath: "/path1"},
221 }),
222 }, []corev1.Container{}, true),
223 },
224 },
225 returnedPVCs: &corev1.PersistentVolumeClaimList{
226 Items: []corev1.PersistentVolumeClaim{
227 *testingutil.FakePVC("volume-0", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
228 *testingutil.FakePVC("volume-1", "10Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-1").Labels()),
229 },
230 },
231 want: StorageList{},
232 wantErr: true,
233 },
234 {
235 name: "case 9: avoid the source volume's mount",
236 fields: fields{
237 generic: generic{
238 appName: "app",
239 componentName: "nodejs",
240 },
241 },
242 returnedDeployments: &appsv1.DeploymentList{
243 Items: []appsv1.Deployment{
244 *testingutil.CreateFakeDeploymentsWithContainers("nodejs", []corev1.Container{
245 testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
246 {Name: "volume-0-vol", MountPath: "/data"},
247 {Name: "volume-1-vol", MountPath: "/path"},
248 }),
249 testingutil.CreateFakeContainerWithVolumeMounts("container-1", []corev1.VolumeMount{
250 {Name: "volume-1-vol", MountPath: "/path"},
251 {Name: OdoSourceVolume, MountPath: "/path1"},
252 }),
253 }, []corev1.Container{}, true),
254 },
255 },
256 returnedPVCs: &corev1.PersistentVolumeClaimList{
257 Items: []corev1.PersistentVolumeClaim{
258 *testingutil.FakePVC("volume-0", "5Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-0").Labels()),
259 *testingutil.FakePVC("volume-1", "10Gi", odolabels.Builder().WithComponent("nodejs").WithDevfileStorageName("volume-1").Labels()),
260 },
261 },
262 want: StorageList{
263 Items: []Storage{
264 generateStorage(NewStorage("volume-0", "5Gi", "/data", nil), "", "container-0"),
265 generateStorage(NewStorage("volume-1", "10Gi", "/path", nil), "", "container-0"),
266 generateStorage(NewStorage("volume-1", "10Gi", "/path", nil), "", "container-1"),
267 },
268 },
269 wantErr: false,
270 },
271 {
272 name: "case 10: avoid the mandatory volume mounts used by odo",
273 fields: fields{
274 generic: generic{
275 appName: "app",
276 componentName: "nodejs",
277 },
278 },
279 returnedDeployments: &appsv1.DeploymentList{
280 Items: []appsv1.Deployment{
281 {
282 ObjectMeta: metav1.ObjectMeta{
283 Name: "nodejs",
284 Labels: map[string]string{
285 "component": "nodejs",
286 },
287 },
288 Spec: appsv1.DeploymentSpec{
289 Template: corev1.PodTemplateSpec{
290 Spec: corev1.PodSpec{
291 InitContainers: []corev1.Container{
292 {
293 Name: "my-container-with-shared-project",
294 VolumeMounts: []corev1.VolumeMount{
295 {
296 Name: "odo-shared-project",
297 MountPath: "/opt/",
298 },
299 },
300 },
301 },
302 },
303 },
304 },
305 },
306 },
307 },
308 returnedPVCs: &corev1.PersistentVolumeClaimList{
309 Items: []corev1.PersistentVolumeClaim{},
310 },
311 want: StorageList{},
312 wantErr: false,
313 },
314 }
315
316 for _, tt := range tests {
317 t.Run(tt.name, func(t *testing.T) {
318 fakeClient, fakeClientSet := kclient.FakeNew()
319
320 fakeClientSet.Kubernetes.PrependReactor("list", "persistentvolumeclaims", func(action ktesting.Action) (bool, runtime.Object, error) {
321 return true, tt.returnedPVCs, nil
322 })
323
324 fakeClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
325 return true, tt.returnedDeployments, nil
326 })
327
328 ctrl := gomock.NewController(t)
329 defer ctrl.Finish()
330
331 k := kubernetesClient{
332 generic: tt.fields.generic,
333 client: fakeClient,
334 }
335 got, err := k.List()
336 if (err != nil) != tt.wantErr {
337 t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr)
338 return
339 }
340 if diff := cmp.Diff(tt.want, got); diff != "" {
341 t.Errorf("kubernetesClient.List() mismatch (-want +got):\n%s", diff)
342 }
343 })
344 }
345 }
346
347 func Test_kubernetesClient_Create(t *testing.T) {
348 type fields struct {
349 generic generic
350 }
351 type args struct {
352 storage Storage
353 }
354 tests := []struct {
355 name string
356 fields fields
357 args args
358 wantErr bool
359 }{
360 {
361 name: "case 1: valid storage",
362 fields: fields{
363 generic: generic{
364 appName: "app",
365 componentName: "nodejs",
366 },
367 },
368 args: args{
369 storage: NewStorageWithContainer("storage-0", "5Gi", "/data", "runtime", util.GetBool(false)),
370 },
371 },
372 {
373 name: "case 2: invalid storage size",
374 fields: fields{
375 generic: generic{
376 appName: "app",
377 componentName: "nodejs",
378 },
379 },
380 args: args{
381 storage: NewStorageWithContainer("storage-0", "example", "/data", "runtime", util.GetBool(false)),
382 },
383 wantErr: true,
384 },
385 {
386 name: "case 3: valid odo project related storage",
387 fields: fields{
388 generic: generic{
389 appName: "app",
390 componentName: "nodejs",
391 },
392 },
393 args: args{
394 storage: NewStorageWithContainer("odo-projects-vol", "5Gi", "/data", "runtime", util.GetBool(false)),
395 },
396 },
397 }
398 for _, tt := range tests {
399 t.Run(tt.name, func(t *testing.T) {
400 fkclient, fkclientset := kclient.FakeNew()
401
402 k := kubernetesClient{
403 generic: tt.fields.generic,
404 client: fkclient,
405 }
406 if err := k.Create(tt.args.storage); (err != nil) != tt.wantErr {
407 t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
408 return
409 }
410 if tt.wantErr == true {
411 return
412 }
413
414
415 if (len(fkclientset.Kubernetes.Actions()) != 1) && (tt.wantErr != true) {
416 t.Errorf("expected 1 action in CreatePVC got: %v", fkclientset.Kubernetes.Actions())
417 return
418 }
419
420 createdPVC := fkclientset.Kubernetes.Actions()[0].(ktesting.CreateAction).GetObject().(*corev1.PersistentVolumeClaim)
421 quantity, err := resource.ParseQuantity(tt.args.storage.Spec.Size)
422 if err != nil {
423 t.Errorf("failed to create quantity by calling resource.ParseQuantity(%v)", tt.args.storage.Spec.Size)
424 }
425
426 wantLabels := odolabels.GetLabels(tt.fields.generic.componentName, tt.fields.generic.appName, "", odolabels.ComponentDevMode, false)
427 odolabels.AddStorageInfo(wantLabels, tt.args.storage.Name, strings.Contains(tt.args.storage.Name, OdoSourceVolume))
428
429
430 if diff := cmp.Diff(wantLabels, createdPVC.Labels); diff != "" {
431 t.Errorf("kubernetesClient.Create() wantLabels mismatch (-want +got):\n%s", diff)
432 }
433
434 if diff := cmp.Diff(quantity, createdPVC.Spec.Resources.Requests["storage"]); diff != "" {
435 t.Errorf("kubernetesClient.Create() quantity mismatch (-want +got):\n%s", diff)
436 }
437
438 wantedPVCName, err := generatePVCName(tt.args.storage.Name, tt.fields.generic.componentName, tt.fields.generic.appName)
439 if err != nil {
440 t.Errorf("unexpected error: %v", err)
441 }
442 if diff := cmp.Diff(wantedPVCName, createdPVC.Name); diff != "" {
443 t.Errorf("kubernetesClient.Create() wantedPVCName mismatch (-want +got):\n%s", diff)
444 }
445 })
446 }
447 }
448
449 func Test_kubernetesClient_Delete(t *testing.T) {
450 pvcName := "pvc-0"
451 returnedPVCs := corev1.PersistentVolumeClaimList{
452 Items: []corev1.PersistentVolumeClaim{
453 *testingutil.FakePVC(pvcName, "5Gi", getStorageLabels("storage-0", "nodejs", "app")),
454 },
455 }
456
457 type fields struct {
458 generic generic
459 }
460 type args struct {
461 name string
462 }
463 tests := []struct {
464 name string
465 fields fields
466 args args
467 wantErr bool
468 }{
469 {
470 name: "case 1: delete successful",
471 fields: fields{
472 generic: generic{
473 appName: "app",
474 componentName: "nodejs",
475 },
476 },
477 args: args{
478 "storage-0",
479 },
480 wantErr: false,
481 },
482 {
483 name: "case 2: pvc not found",
484 fields: fields{
485 generic: generic{
486 appName: "app",
487 componentName: "nodejs",
488 },
489 },
490 args: args{
491 "storage-example",
492 },
493 wantErr: true,
494 },
495 }
496 for _, tt := range tests {
497 t.Run(tt.name, func(t *testing.T) {
498 fkclient, fkclientset := kclient.FakeNew()
499
500 fkclientset.Kubernetes.PrependReactor("list", "persistentvolumeclaims", func(action ktesting.Action) (bool, runtime.Object, error) {
501 return true, &returnedPVCs, nil
502 })
503
504 fkclientset.Kubernetes.PrependReactor("delete", "persistentvolumeclaims", func(action ktesting.Action) (bool, runtime.Object, error) {
505 if action.(ktesting.DeleteAction).GetName() != pvcName {
506 t.Errorf("delete called with = %v, want %v", action.(ktesting.DeleteAction).GetName(), pvcName)
507 }
508 return true, nil, nil
509 })
510
511 k := kubernetesClient{
512 generic: tt.fields.generic,
513 client: fkclient,
514 }
515 if err := k.Delete(tt.args.name); (err != nil) != tt.wantErr {
516 t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
517 return
518 }
519
520 if tt.wantErr {
521 return
522 }
523
524 if len(fkclientset.Kubernetes.Actions()) != 2 {
525 t.Errorf("expected 2 action, got %v", len(fkclientset.Kubernetes.Actions()))
526 }
527 })
528 }
529 }
530
View as plain text