github.com/openebs/node-disk-manager@v1.9.1-0.20230225014141-4531f06ffa1e/pkg/controllers/blockdeviceclaim/blockdeviceclaim_controller_test.go (about) 1 /* 2 Copyright 2019 The OpenEBS 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 blockdeviceclaim 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 "time" 24 25 "github.com/openebs/node-disk-manager/db/kubernetes" 26 27 openebsv1alpha1 "github.com/openebs/node-disk-manager/api/v1alpha1" 28 ndm "github.com/openebs/node-disk-manager/cmd/ndm_daemonset/controller" 29 30 "github.com/stretchr/testify/assert" 31 corev1 "k8s.io/api/core/v1" 32 "k8s.io/apimachinery/pkg/api/errors" 33 "k8s.io/apimachinery/pkg/api/resource" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/types" 37 "k8s.io/client-go/kubernetes/scheme" 38 "k8s.io/client-go/tools/record" 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 "sigs.k8s.io/controller-runtime/pkg/client/fake" 41 logf "sigs.k8s.io/controller-runtime/pkg/log" 42 "sigs.k8s.io/controller-runtime/pkg/log/zap" 43 "sigs.k8s.io/controller-runtime/pkg/reconcile" 44 ) 45 46 var ( 47 fakeHostName = "fake-hostname" 48 diskName = "disk-example" 49 deviceName = "blockdevice-example" 50 blockDeviceClaimName = "blockdeviceclaim-example" 51 blockDeviceClaimUID types.UID = "blockDeviceClaim-example-UID" 52 namespace = "" 53 capacity uint64 = 1024000 54 claimCapacity = resource.MustParse("1024000") 55 fakeRecorder = record.NewFakeRecorder(50) 56 ) 57 58 // TestBlockDeviceClaimController runs ReconcileBlockDeviceClaim.Reconcile() against a 59 // fake client that tracks a BlockDeviceClaim object. 60 func TestBlockDeviceClaimController(t *testing.T) { 61 62 // Set the logger to development mode for verbose logs. 63 logf.SetLogger(zap.New(zap.UseDevMode(true))) 64 65 // Create a fake client to mock API calls. 66 cl, s := CreateFakeClient() 67 deviceR := GetFakeDeviceObject(deviceName, capacity) 68 deviceR.Labels[kubernetes.KubernetesHostNameLabel] = fakeHostName 69 70 deviceClaimR := GetFakeBlockDeviceClaimObject() 71 // Create a new blockdevice obj 72 err := cl.Create(context.TODO(), deviceR) 73 if err != nil { 74 fmt.Println("BlockDevice object is not created", err) 75 } 76 77 // Create a new deviceclaim obj 78 err = cl.Create(context.TODO(), deviceClaimR) 79 if err != nil { 80 fmt.Println("BlockDeviceClaim object is not created", err) 81 } 82 83 // Create a ReconcileDevice object with the scheme and fake client. 84 r := &BlockDeviceClaimReconciler{Client: cl, Scheme: s, Recorder: fakeRecorder} 85 86 // Mock request to simulate Reconcile() being called on an event for a 87 // watched resource . 88 req := reconcile.Request{ 89 NamespacedName: types.NamespacedName{ 90 Name: blockDeviceClaimName, 91 Namespace: namespace, 92 }, 93 } 94 95 // Check status of deviceClaim it should be empty(Not bound) 96 r.CheckBlockDeviceClaimStatus(t, req, openebsv1alpha1.BlockDeviceClaimStatusEmpty) 97 98 // Fetch the BlockDeviceClaim CR and change capacity to invalid 99 // Since Capacity is invalid, it delete device claim CR 100 r.InvalidCapacityTest(t, req) 101 102 // Create new BlockDeviceClaim CR with right capacity, 103 // trigger reconciliation event. This time, it should 104 // bound. 105 deviceClaim := &openebsv1alpha1.BlockDeviceClaim{} 106 err = r.Client.Get(context.TODO(), req.NamespacedName, deviceClaim) 107 if err != nil { 108 t.Errorf("Get deviceClaim: (%v)", err) 109 } 110 orignalDeviceClaim := deviceClaim.DeepCopy() 111 deviceClaim.Spec.Resources.Requests[openebsv1alpha1.ResourceStorage] = claimCapacity 112 // resetting status to empty 113 deviceClaim.Status.Phase = openebsv1alpha1.BlockDeviceClaimStatusEmpty 114 err = r.Client.Patch(context.TODO(), deviceClaim, client.MergeFrom(orignalDeviceClaim)) 115 if err != nil { 116 t.Errorf("Update deviceClaim: (%v)", err) 117 } 118 119 res, err := r.Reconcile(context.TODO(), req) 120 if err != nil { 121 t.Logf("reconcile: (%v)", err) 122 } 123 124 // Check the result of reconciliation to make sure it has the desired state. 125 if !res.Requeue { 126 t.Log("reconcile did not requeue request as expected") 127 } 128 r.CheckBlockDeviceClaimStatus(t, req, openebsv1alpha1.BlockDeviceClaimStatusDone) 129 130 r.DeviceRequestedHappyPathTest(t, req) 131 //TODO: Need to find a way to update deletion timestamp 132 //r.DeleteBlockDeviceClaimedTest(t, req) 133 } 134 135 func (r *BlockDeviceClaimReconciler) DeleteBlockDeviceClaimedTest(t *testing.T, 136 req reconcile.Request) { 137 138 devRequestInst := &openebsv1alpha1.BlockDeviceClaim{} 139 140 // Fetch the BlockDeviceClaim CR 141 err := r.Client.Get(context.TODO(), req.NamespacedName, devRequestInst) 142 if err != nil { 143 t.Errorf("Get devClaimInst: (%v)", err) 144 } 145 146 err = r.Client.Delete(context.TODO(), devRequestInst) 147 if err != nil { 148 t.Errorf("Delete devClaimInst: (%v)", err) 149 } 150 151 res, err := r.Reconcile(context.TODO(), req) 152 if err != nil { 153 t.Logf("reconcile: (%v)", err) 154 } 155 156 // Check the result of reconciliation to make sure it has the desired state. 157 if !res.Requeue { 158 t.Log("reconcile did not requeue request as expected") 159 } 160 161 dvRequestInst := &openebsv1alpha1.BlockDeviceClaim{} 162 err = r.Client.Get(context.TODO(), req.NamespacedName, dvRequestInst) 163 if errors.IsNotFound(err) { 164 t.Logf("BlockDeviceClaim is deleted, expected") 165 err = nil 166 } else if err != nil { 167 t.Errorf("Get dvClaimInst: (%v)", err) 168 } 169 170 time.Sleep(10 * time.Second) 171 // Fetch the BlockDevice CR 172 devInst := &openebsv1alpha1.BlockDevice{} 173 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: deviceName, Namespace: namespace}, devInst) 174 if err != nil { 175 t.Errorf("get devInst: (%v)", err) 176 } 177 178 if devInst.Spec.ClaimRef.UID == dvRequestInst.ObjectMeta.UID { 179 t.Logf("BlockDevice ObjRef UID:%v match expected deviceRequest UID:%v", 180 devInst.Spec.ClaimRef.UID, dvRequestInst.ObjectMeta.UID) 181 } else { 182 t.Fatalf("BlockDevice ObjRef UID:%v did not match expected deviceRequest UID:%v", 183 devInst.Spec.ClaimRef.UID, dvRequestInst.ObjectMeta.UID) 184 } 185 186 if devInst.Status.ClaimState == openebsv1alpha1.BlockDeviceClaimed { 187 t.Logf("BlockDevice Obj state:%v match expected state:%v", 188 devInst.Status.ClaimState, openebsv1alpha1.BlockDeviceClaimed) 189 } else { 190 t.Fatalf("BlockDevice Obj state:%v did not match expected state:%v", 191 devInst.Status.ClaimState, openebsv1alpha1.BlockDeviceClaimed) 192 } 193 } 194 195 func (r *BlockDeviceClaimReconciler) DeviceRequestedHappyPathTest(t *testing.T, 196 req reconcile.Request) { 197 198 devRequestInst := &openebsv1alpha1.BlockDeviceClaim{} 199 // Fetch the BlockDeviceClaim CR 200 err := r.Client.Get(context.TODO(), req.NamespacedName, devRequestInst) 201 if err != nil { 202 t.Errorf("Get devRequestInst: (%v)", err) 203 } 204 205 // Fetch the BlockDevice CR 206 devInst := &openebsv1alpha1.BlockDevice{} 207 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: deviceName, Namespace: namespace}, devInst) 208 if err != nil { 209 t.Errorf("get devInst: (%v)", err) 210 } 211 212 if devInst.Spec.ClaimRef.UID == devRequestInst.ObjectMeta.UID { 213 t.Logf("BlockDevice ObjRef UID:%v match expected deviceRequest UID:%v", 214 devInst.Spec.ClaimRef.UID, devRequestInst.ObjectMeta.UID) 215 } else { 216 t.Fatalf("BlockDevice ObjRef UID:%v did not match expected deviceRequest UID:%v", 217 devInst.Spec.ClaimRef.UID, devRequestInst.ObjectMeta.UID) 218 } 219 220 if devInst.Status.ClaimState == openebsv1alpha1.BlockDeviceClaimed { 221 t.Logf("BlockDevice Obj state:%v match expected state:%v", 222 devInst.Status.ClaimState, openebsv1alpha1.BlockDeviceClaimed) 223 } else { 224 t.Fatalf("BlockDevice Obj state:%v did not match expected state:%v", 225 devInst.Status.ClaimState, openebsv1alpha1.BlockDeviceClaimed) 226 } 227 } 228 229 func (r *BlockDeviceClaimReconciler) InvalidCapacityTest(t *testing.T, 230 req reconcile.Request) { 231 232 devRequestInst := &openebsv1alpha1.BlockDeviceClaim{} 233 err := r.Client.Get(context.TODO(), req.NamespacedName, devRequestInst) 234 if err != nil { 235 t.Errorf("Get devRequestInst: (%v)", err) 236 } 237 238 orignalDevRequestInst := devRequestInst.DeepCopy() 239 devRequestInst.Spec.Resources.Requests[openebsv1alpha1.ResourceStorage] = resource.MustParse("0") 240 err = r.Client.Patch(context.TODO(), devRequestInst, client.MergeFrom(orignalDevRequestInst)) 241 if err != nil { 242 t.Errorf("Update devRequestInst: (%v)", err) 243 } 244 245 res, err := r.Reconcile(context.TODO(), req) 246 if err != nil { 247 t.Logf("reconcile: (%v)", err) 248 } 249 250 // Check the result of reconciliation to make sure it has the desired state. 251 if !res.Requeue { 252 t.Log("reconcile did not requeue request as expected") 253 } 254 255 dvC := &openebsv1alpha1.BlockDeviceClaim{} 256 err = r.Client.Get(context.TODO(), req.NamespacedName, dvC) 257 if err != nil { 258 t.Errorf("Get devRequestInst: (%v)", err) 259 } 260 r.CheckBlockDeviceClaimStatus(t, req, openebsv1alpha1.BlockDeviceClaimStatusPending) 261 } 262 263 func TestBlockDeviceClaimsLabelSelector(t *testing.T) { 264 // Set the logger to development mode for verbose logs. 265 logf.SetLogger(zap.New(zap.UseDevMode(true))) 266 267 tests := map[string]struct { 268 bdLabels map[string]string 269 selector *metav1.LabelSelector 270 expectedClaimPhase openebsv1alpha1.DeviceClaimPhase 271 }{ 272 "only hostname label is present and no selector": { 273 bdLabels: map[string]string{ 274 kubernetes.KubernetesHostNameLabel: fakeHostName, 275 }, 276 selector: nil, 277 expectedClaimPhase: openebsv1alpha1.BlockDeviceClaimStatusDone, 278 }, 279 "custom label and hostname present on bd and selector": { 280 bdLabels: map[string]string{ 281 ndm.KubernetesHostNameLabel: fakeHostName, 282 "ndm.io/test": "1234", 283 }, 284 selector: &metav1.LabelSelector{ 285 MatchLabels: map[string]string{ 286 ndm.KubernetesHostNameLabel: fakeHostName, 287 "ndm.io/test": "1234", 288 }, 289 }, 290 expectedClaimPhase: openebsv1alpha1.BlockDeviceClaimStatusDone, 291 }, 292 "custom labels and hostname": { 293 bdLabels: map[string]string{ 294 ndm.KubernetesHostNameLabel: fakeHostName, 295 "ndm.io/test": "1234", 296 }, 297 selector: &metav1.LabelSelector{ 298 MatchLabels: map[string]string{ 299 "ndm.io/test": "1234", 300 }, 301 }, 302 expectedClaimPhase: openebsv1alpha1.BlockDeviceClaimStatusDone, 303 }, 304 } 305 for name, test := range tests { 306 t.Run(name, func(t *testing.T) { 307 // pinning the variables 308 bdLabels := test.bdLabels 309 selector := test.selector 310 expectedClaimPhase := test.expectedClaimPhase 311 312 // Create a fake client to mock API calls. 313 cl, s := CreateFakeClient() 314 315 // Create a ReconcileDevice object with the scheme and fake client. 316 r := &BlockDeviceClaimReconciler{Client: cl, Scheme: s, Recorder: fakeRecorder} 317 318 // Mock request to simulate Reconcile() being called on an event for a 319 // watched resource . 320 req := reconcile.Request{ 321 NamespacedName: types.NamespacedName{ 322 Name: blockDeviceClaimName, 323 Namespace: namespace, 324 }, 325 } 326 327 bdcR := GetFakeBlockDeviceClaimObject() 328 if err := cl.Create(context.TODO(), bdcR); err != nil { 329 t.Fatal(err) 330 } 331 332 bd := GetFakeDeviceObject("bd-1", capacity*10) 333 334 bd.Labels = bdLabels 335 // mark the device as unclaimed 336 bd.Spec.ClaimRef = nil 337 bd.Status.ClaimState = openebsv1alpha1.BlockDeviceUnclaimed 338 339 err := cl.Create(context.TODO(), bd) 340 if err != nil { 341 t.Fatalf("error updating BD. %v", err) 342 } 343 bdc := GetFakeBlockDeviceClaimObject() 344 orignalBDC := bdc.DeepCopy() 345 bdc.Spec.BlockDeviceName = "" 346 bdc.Spec.Selector = selector 347 err = cl.Patch(context.TODO(), bdc, client.MergeFrom(orignalBDC)) 348 if err != nil { 349 t.Fatalf("error updating BDC. %v", err) 350 } 351 _, _ = r.Reconcile(context.TODO(), req) 352 353 err = cl.Get(context.TODO(), req.NamespacedName, bdc) 354 if err != nil { 355 t.Fatalf("error getting BDC. %v", err) 356 } 357 358 err = cl.Delete(context.TODO(), bd) 359 if err != nil { 360 t.Fatalf("error deleting BDC. %v", err) 361 } 362 assert.Equal(t, expectedClaimPhase, bdc.Status.Phase) 363 }) 364 } 365 } 366 367 func (r *BlockDeviceClaimReconciler) CheckBlockDeviceClaimStatus(t *testing.T, 368 req reconcile.Request, phase openebsv1alpha1.DeviceClaimPhase) { 369 370 devRequestCR := &openebsv1alpha1.BlockDeviceClaim{} 371 err := r.Client.Get(context.TODO(), req.NamespacedName, devRequestCR) 372 if err != nil { 373 t.Errorf("get devRequestCR : (%v)", err) 374 } 375 376 // BlockDeviceClaim should yet to bound. 377 if devRequestCR.Status.Phase == phase { 378 t.Logf("BlockDeviceClaim Object status:%v match expected status:%v", 379 devRequestCR.Status.Phase, phase) 380 } else { 381 t.Fatalf("BlockDeviceClaim Object status:%v did not match expected status:%v", 382 devRequestCR.Status.Phase, phase) 383 } 384 } 385 386 func GetFakeBlockDeviceClaimObject() *openebsv1alpha1.BlockDeviceClaim { 387 deviceRequestCR := &openebsv1alpha1.BlockDeviceClaim{} 388 389 TypeMeta := metav1.TypeMeta{ 390 Kind: "BlockDeviceClaim", 391 APIVersion: ndm.NDMVersion, 392 } 393 394 ObjectMeta := metav1.ObjectMeta{ 395 Labels: make(map[string]string), 396 Name: blockDeviceClaimName, 397 Namespace: namespace, 398 UID: blockDeviceClaimUID, 399 } 400 401 Requests := corev1.ResourceList{openebsv1alpha1.ResourceStorage: claimCapacity} 402 403 Requirements := openebsv1alpha1.DeviceClaimResources{ 404 Requests: Requests, 405 } 406 407 Spec := openebsv1alpha1.DeviceClaimSpec{ 408 Resources: Requirements, 409 DeviceType: "", 410 HostName: fakeHostName, 411 } 412 413 deviceRequestCR.ObjectMeta = ObjectMeta 414 deviceRequestCR.TypeMeta = TypeMeta 415 deviceRequestCR.Spec = Spec 416 deviceRequestCR.Status.Phase = openebsv1alpha1.BlockDeviceClaimStatusEmpty 417 return deviceRequestCR 418 } 419 420 func GetFakeDeviceObject(bdName string, bdCapacity uint64) *openebsv1alpha1.BlockDevice { 421 device := &openebsv1alpha1.BlockDevice{} 422 423 TypeMeta := metav1.TypeMeta{ 424 Kind: ndm.NDMBlockDeviceKind, 425 APIVersion: ndm.NDMVersion, 426 } 427 428 ObjectMeta := metav1.ObjectMeta{ 429 Labels: make(map[string]string), 430 Name: bdName, 431 Namespace: namespace, 432 } 433 434 Spec := openebsv1alpha1.DeviceSpec{ 435 Path: "dev/disk-fake-path", 436 Capacity: openebsv1alpha1.DeviceCapacity{ 437 Storage: bdCapacity, // Set blockdevice size. 438 }, 439 DevLinks: make([]openebsv1alpha1.DeviceDevLink, 0), 440 Partitioned: ndm.NDMNotPartitioned, 441 } 442 443 device.ObjectMeta = ObjectMeta 444 device.TypeMeta = TypeMeta 445 device.Status.ClaimState = openebsv1alpha1.BlockDeviceUnclaimed 446 device.Status.State = ndm.NDMActive 447 device.Spec = Spec 448 return device 449 } 450 451 func CreateFakeClient() (client.Client, *runtime.Scheme) { 452 453 deviceR := GetFakeDeviceObject(deviceName, capacity) 454 455 deviceList := &openebsv1alpha1.BlockDeviceList{ 456 TypeMeta: metav1.TypeMeta{ 457 Kind: "BlockDevice", 458 APIVersion: "", 459 }, 460 } 461 462 deviceClaimR := GetFakeBlockDeviceClaimObject() 463 deviceclaimList := &openebsv1alpha1.BlockDeviceClaimList{ 464 TypeMeta: metav1.TypeMeta{ 465 Kind: "BlockDeviceClaim", 466 APIVersion: "", 467 }, 468 } 469 470 s := scheme.Scheme 471 472 s.AddKnownTypes(openebsv1alpha1.GroupVersion, deviceR) 473 s.AddKnownTypes(openebsv1alpha1.GroupVersion, deviceList) 474 s.AddKnownTypes(openebsv1alpha1.GroupVersion, deviceClaimR) 475 s.AddKnownTypes(openebsv1alpha1.GroupVersion, deviceclaimList) 476 477 fakeNdmClient := fake.NewFakeClientWithScheme(s) 478 if fakeNdmClient == nil { 479 fmt.Println("NDMClient is not created") 480 } 481 482 return fakeNdmClient, s 483 } 484 485 func TestGenerateSelector(t *testing.T) { 486 tests := map[string]struct { 487 bdc openebsv1alpha1.BlockDeviceClaim 488 want *metav1.LabelSelector 489 }{ 490 "hostname/node attributes not given and no selector": { 491 bdc: openebsv1alpha1.BlockDeviceClaim{ 492 Spec: openebsv1alpha1.DeviceClaimSpec{}, 493 }, 494 want: &metav1.LabelSelector{ 495 MatchLabels: make(map[string]string), 496 }, 497 }, 498 "hostname is given, node attributes not given and no selector": { 499 bdc: openebsv1alpha1.BlockDeviceClaim{ 500 Spec: openebsv1alpha1.DeviceClaimSpec{ 501 HostName: "hostname", 502 }, 503 }, 504 want: &metav1.LabelSelector{ 505 MatchLabels: map[string]string{ 506 ndm.KubernetesHostNameLabel: "hostname", 507 }, 508 }, 509 }, 510 "hostname is not given, node attribute is given and no selector": { 511 bdc: openebsv1alpha1.BlockDeviceClaim{ 512 Spec: openebsv1alpha1.DeviceClaimSpec{ 513 BlockDeviceNodeAttributes: openebsv1alpha1.BlockDeviceNodeAttributes{ 514 HostName: "hostname", 515 }, 516 }, 517 }, 518 want: &metav1.LabelSelector{ 519 MatchLabels: map[string]string{ 520 ndm.KubernetesHostNameLabel: "hostname", 521 }, 522 }, 523 }, 524 "same hostname, node attribute is given and no selector": { 525 bdc: openebsv1alpha1.BlockDeviceClaim{ 526 Spec: openebsv1alpha1.DeviceClaimSpec{ 527 HostName: "hostname", 528 BlockDeviceNodeAttributes: openebsv1alpha1.BlockDeviceNodeAttributes{ 529 HostName: "hostname", 530 }, 531 }, 532 }, 533 want: &metav1.LabelSelector{ 534 MatchLabels: map[string]string{ 535 ndm.KubernetesHostNameLabel: "hostname", 536 }, 537 }, 538 }, 539 "different hostname and node attributes is given and no selector": { 540 bdc: openebsv1alpha1.BlockDeviceClaim{ 541 Spec: openebsv1alpha1.DeviceClaimSpec{ 542 HostName: "hostname1", 543 BlockDeviceNodeAttributes: openebsv1alpha1.BlockDeviceNodeAttributes{ 544 HostName: "hostname2", 545 }, 546 }, 547 }, 548 want: &metav1.LabelSelector{ 549 MatchLabels: map[string]string{ 550 ndm.KubernetesHostNameLabel: "hostname2", 551 }, 552 }, 553 }, 554 "no hostname and custom selector is given": { 555 bdc: openebsv1alpha1.BlockDeviceClaim{ 556 Spec: openebsv1alpha1.DeviceClaimSpec{ 557 Selector: &metav1.LabelSelector{ 558 MatchLabels: map[string]string{ 559 "ndm.io/test": "test", 560 }, 561 }, 562 }, 563 }, 564 want: &metav1.LabelSelector{ 565 MatchLabels: map[string]string{ 566 "ndm.io/test": "test", 567 }, 568 }, 569 }, 570 "hostname given and selector also contains custom label name": { 571 bdc: openebsv1alpha1.BlockDeviceClaim{ 572 Spec: openebsv1alpha1.DeviceClaimSpec{ 573 Selector: &metav1.LabelSelector{ 574 MatchLabels: map[string]string{ 575 ndm.KubernetesHostNameLabel: "hostname1", 576 "ndm.io/test": "test", 577 }, 578 }, 579 HostName: "hostname2", 580 }, 581 }, 582 want: &metav1.LabelSelector{ 583 MatchLabels: map[string]string{ 584 ndm.KubernetesHostNameLabel: "hostname2", 585 "ndm.io/test": "test", 586 }, 587 }, 588 }, 589 } 590 for name, test := range tests { 591 t.Run(name, func(t *testing.T) { 592 got := generateSelector(test.bdc) 593 assert.Equal(t, test.want, got) 594 }) 595 } 596 }