k8s.io/kubernetes@v1.29.3/pkg/controller/volume/persistentvolume/testing/testing.go (about) 1 /* 2 Copyright 2019 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 testing 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "reflect" 24 "strconv" 25 "sync" 26 27 "github.com/google/go-cmp/cmp" 28 "k8s.io/klog/v2" 29 30 v1 "k8s.io/api/core/v1" 31 apierrors "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/watch" 36 "k8s.io/client-go/kubernetes/fake" 37 core "k8s.io/client-go/testing" 38 ) 39 40 // ErrVersionConflict is the error returned when resource version of requested 41 // object conflicts with the object in storage. 42 var ErrVersionConflict = errors.New("VersionError") 43 44 // VolumeReactor is a core.Reactor that simulates etcd and API server. It 45 // stores: 46 // - Latest version of claims volumes saved by the controller. 47 // - Queue of all saves (to simulate "volume/claim updated" events). This queue 48 // contains all intermediate state of an object - e.g. a claim.VolumeName 49 // is updated first and claim.Phase second. This queue will then contain both 50 // updates as separate entries. 51 // - Number of changes since the last call to VolumeReactor.syncAll(). 52 // - Optionally, volume and claim fake watchers which should be the same ones 53 // used by the controller. Any time an event function like deleteVolumeEvent 54 // is called to simulate an event, the reactor's stores are updated and the 55 // controller is sent the event via the fake watcher. 56 // - Optionally, list of error that should be returned by reactor, simulating 57 // etcd / API server failures. These errors are evaluated in order and every 58 // error is returned only once. I.e. when the reactor finds matching 59 // ReactorError, it return appropriate error and removes the ReactorError from 60 // the list. 61 type VolumeReactor struct { 62 volumes map[string]*v1.PersistentVolume 63 claims map[string]*v1.PersistentVolumeClaim 64 changedObjects []interface{} 65 changedSinceLastSync int 66 fakeVolumeWatch *watch.FakeWatcher 67 fakeClaimWatch *watch.FakeWatcher 68 lock sync.RWMutex 69 errors []ReactorError 70 watchers map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher 71 } 72 73 // ReactorError is an error that is returned by test reactor (=simulated 74 // etcd+/API server) when an action performed by the reactor matches given verb 75 // ("get", "update", "create", "delete" or "*"") on given resource 76 // ("persistentvolumes", "persistentvolumeclaims" or "*"). 77 type ReactorError struct { 78 Verb string 79 Resource string 80 Error error 81 } 82 83 // React is a callback called by fake kubeClient from the controller. 84 // In other words, every claim/volume change performed by the controller ends 85 // here. 86 // This callback checks versions of the updated objects and refuse those that 87 // are too old (simulating real etcd). 88 // All updated objects are stored locally to keep track of object versions and 89 // to evaluate test results. 90 // All updated objects are also inserted into changedObjects queue and 91 // optionally sent back to the controller via its watchers. 92 func (r *VolumeReactor) React(ctx context.Context, action core.Action) (handled bool, ret runtime.Object, err error) { 93 r.lock.Lock() 94 defer r.lock.Unlock() 95 logger := klog.FromContext(ctx) 96 logger.V(4).Info("Reactor got operation", "resource", action.GetResource(), "verb", action.GetVerb()) 97 98 // Inject error when requested 99 err = r.injectReactError(ctx, action) 100 if err != nil { 101 return true, nil, err 102 } 103 104 // Test did not request to inject an error, continue simulating API server. 105 switch { 106 case action.Matches("create", "persistentvolumes"): 107 obj := action.(core.UpdateAction).GetObject() 108 volume := obj.(*v1.PersistentVolume) 109 110 // check the volume does not exist 111 _, found := r.volumes[volume.Name] 112 if found { 113 return true, nil, fmt.Errorf("cannot create volume %s: volume already exists", volume.Name) 114 } 115 116 // mimic apiserver defaulting 117 if volume.Spec.VolumeMode == nil { 118 volume.Spec.VolumeMode = new(v1.PersistentVolumeMode) 119 *volume.Spec.VolumeMode = v1.PersistentVolumeFilesystem 120 } 121 122 // Store the updated object to appropriate places. 123 r.volumes[volume.Name] = volume 124 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 125 w.Add(volume) 126 } 127 r.changedObjects = append(r.changedObjects, volume) 128 r.changedSinceLastSync++ 129 logger.V(4).Info("Created volume", "volumeName", volume.Name) 130 return true, volume, nil 131 132 case action.Matches("create", "persistentvolumeclaims"): 133 obj := action.(core.UpdateAction).GetObject() 134 claim := obj.(*v1.PersistentVolumeClaim) 135 136 // check the claim does not exist 137 _, found := r.claims[claim.Name] 138 if found { 139 return true, nil, fmt.Errorf("cannot create claim %s: claim already exists", claim.Name) 140 } 141 142 // Store the updated object to appropriate places. 143 r.claims[claim.Name] = claim 144 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 145 w.Add(claim) 146 } 147 r.changedObjects = append(r.changedObjects, claim) 148 r.changedSinceLastSync++ 149 logger.V(4).Info("Created claim", "PVC", klog.KObj(claim)) 150 return true, claim, nil 151 152 case action.Matches("update", "persistentvolumes"): 153 obj := action.(core.UpdateAction).GetObject() 154 volume := obj.(*v1.PersistentVolume) 155 156 // Check and bump object version 157 storedVolume, found := r.volumes[volume.Name] 158 if found { 159 storedVer, _ := strconv.Atoi(storedVolume.ResourceVersion) 160 requestedVer, _ := strconv.Atoi(volume.ResourceVersion) 161 if storedVer != requestedVer { 162 return true, obj, ErrVersionConflict 163 } 164 if reflect.DeepEqual(storedVolume, volume) { 165 logger.V(4).Info("Nothing updated volume", "volumeName", volume.Name) 166 return true, volume, nil 167 } 168 // Don't modify the existing object 169 volume = volume.DeepCopy() 170 volume.ResourceVersion = strconv.Itoa(storedVer + 1) 171 } else { 172 return true, nil, fmt.Errorf("cannot update volume %s: volume not found", volume.Name) 173 } 174 175 // Store the updated object to appropriate places. 176 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 177 w.Modify(volume) 178 } 179 r.volumes[volume.Name] = volume 180 r.changedObjects = append(r.changedObjects, volume) 181 r.changedSinceLastSync++ 182 logger.V(4).Info("Saved updated volume", "volumeName", volume.Name) 183 return true, volume, nil 184 185 case action.Matches("update", "persistentvolumeclaims"): 186 obj := action.(core.UpdateAction).GetObject() 187 claim := obj.(*v1.PersistentVolumeClaim) 188 189 // Check and bump object version 190 storedClaim, found := r.claims[claim.Name] 191 if found { 192 storedVer, _ := strconv.Atoi(storedClaim.ResourceVersion) 193 requestedVer, _ := strconv.Atoi(claim.ResourceVersion) 194 if storedVer != requestedVer { 195 return true, obj, ErrVersionConflict 196 } 197 if reflect.DeepEqual(storedClaim, claim) { 198 logger.V(4).Info("Nothing updated claim", "PVC", klog.KObj(claim)) 199 return true, claim, nil 200 } 201 // Don't modify the existing object 202 claim = claim.DeepCopy() 203 claim.ResourceVersion = strconv.Itoa(storedVer + 1) 204 } else { 205 return true, nil, fmt.Errorf("cannot update claim %s: claim not found", claim.Name) 206 } 207 208 // Store the updated object to appropriate places. 209 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 210 w.Modify(claim) 211 } 212 r.claims[claim.Name] = claim 213 r.changedObjects = append(r.changedObjects, claim) 214 r.changedSinceLastSync++ 215 logger.V(4).Info("Saved updated claim", "PVC", klog.KObj(claim)) 216 return true, claim, nil 217 218 case action.Matches("get", "persistentvolumes"): 219 name := action.(core.GetAction).GetName() 220 volume, found := r.volumes[name] 221 if found { 222 logger.V(4).Info("GetVolume: found volume", "volumeName", volume.Name) 223 return true, volume.DeepCopy(), nil 224 } 225 logger.V(4).Info("GetVolume: volume not found", "volumeName", name) 226 return true, nil, apierrors.NewNotFound(action.GetResource().GroupResource(), name) 227 228 case action.Matches("get", "persistentvolumeclaims"): 229 name := action.(core.GetAction).GetName() 230 nameSpace := action.(core.GetAction).GetNamespace() 231 claim, found := r.claims[name] 232 if found { 233 logger.V(4).Info("GetClaim: found claim", "PVC", klog.KObj(claim)) 234 return true, claim.DeepCopy(), nil 235 } 236 logger.V(4).Info("GetClaim: claim not found", "PVC", klog.KRef(nameSpace, name)) 237 return true, nil, apierrors.NewNotFound(action.GetResource().GroupResource(), name) 238 239 case action.Matches("delete", "persistentvolumes"): 240 name := action.(core.DeleteAction).GetName() 241 logger.V(4).Info("Deleted volume", "volumeName", name) 242 obj, found := r.volumes[name] 243 if found { 244 delete(r.volumes, name) 245 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 246 w.Delete(obj) 247 } 248 r.changedSinceLastSync++ 249 return true, nil, nil 250 } 251 return true, nil, fmt.Errorf("cannot delete volume %s: not found", name) 252 253 case action.Matches("delete", "persistentvolumeclaims"): 254 name := action.(core.DeleteAction).GetName() 255 nameSpace := action.(core.DeleteAction).GetNamespace() 256 logger.V(4).Info("Deleted claim", "PVC", klog.KRef(nameSpace, name)) 257 obj, found := r.claims[name] 258 if found { 259 delete(r.claims, name) 260 for _, w := range r.getWatches(action.GetResource(), action.GetNamespace()) { 261 w.Delete(obj) 262 } 263 r.changedSinceLastSync++ 264 return true, nil, nil 265 } 266 return true, nil, fmt.Errorf("cannot delete claim %s: not found", name) 267 } 268 269 return false, nil, nil 270 } 271 272 // Watch watches objects from the VolumeReactor. Watch returns a channel which 273 // will push added / modified / deleted object. 274 func (r *VolumeReactor) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) { 275 r.lock.Lock() 276 defer r.lock.Unlock() 277 278 fakewatcher := watch.NewRaceFreeFake() 279 280 if _, exists := r.watchers[gvr]; !exists { 281 r.watchers[gvr] = make(map[string][]*watch.RaceFreeFakeWatcher) 282 } 283 r.watchers[gvr][ns] = append(r.watchers[gvr][ns], fakewatcher) 284 return fakewatcher, nil 285 } 286 287 func (r *VolumeReactor) getWatches(gvr schema.GroupVersionResource, ns string) []*watch.RaceFreeFakeWatcher { 288 watches := []*watch.RaceFreeFakeWatcher{} 289 if r.watchers[gvr] != nil { 290 if w := r.watchers[gvr][ns]; w != nil { 291 watches = append(watches, w...) 292 } 293 if ns != metav1.NamespaceAll { 294 if w := r.watchers[gvr][metav1.NamespaceAll]; w != nil { 295 watches = append(watches, w...) 296 } 297 } 298 } 299 return watches 300 } 301 302 // injectReactError returns an error when the test requested given action to 303 // fail. nil is returned otherwise. 304 func (r *VolumeReactor) injectReactError(ctx context.Context, action core.Action) error { 305 if len(r.errors) == 0 { 306 // No more errors to inject, everything should succeed. 307 return nil 308 } 309 logger := klog.FromContext(ctx) 310 for i, expected := range r.errors { 311 logger.V(4).Info("Trying to match resource verb", "resource", action.GetResource(), "verb", action.GetVerb(), "expectedResource", expected.Resource, "expectedVerb", expected.Verb) 312 if action.Matches(expected.Verb, expected.Resource) { 313 // That's the action we're waiting for, remove it from injectedErrors 314 r.errors = append(r.errors[:i], r.errors[i+1:]...) 315 logger.V(4).Info("Reactor found matching error", "index", i, "expectedResource", expected.Resource, "expectedVerb", expected.Verb, "err", expected.Error) 316 return expected.Error 317 } 318 } 319 return nil 320 } 321 322 // CheckVolumes compares all expectedVolumes with set of volumes at the end of 323 // the test and reports differences. 324 func (r *VolumeReactor) CheckVolumes(expectedVolumes []*v1.PersistentVolume) error { 325 r.lock.Lock() 326 defer r.lock.Unlock() 327 328 expectedMap := make(map[string]*v1.PersistentVolume) 329 gotMap := make(map[string]*v1.PersistentVolume) 330 // Clear any ResourceVersion from both sets 331 for _, v := range expectedVolumes { 332 // Don't modify the existing object 333 v := v.DeepCopy() 334 v.ResourceVersion = "" 335 if v.Spec.ClaimRef != nil { 336 v.Spec.ClaimRef.ResourceVersion = "" 337 } 338 expectedMap[v.Name] = v 339 } 340 for _, v := range r.volumes { 341 // We must clone the volume because of golang race check - it was 342 // written by the controller without any locks on it. 343 v := v.DeepCopy() 344 v.ResourceVersion = "" 345 if v.Spec.ClaimRef != nil { 346 v.Spec.ClaimRef.ResourceVersion = "" 347 } 348 gotMap[v.Name] = v 349 } 350 if !reflect.DeepEqual(expectedMap, gotMap) { 351 // Print ugly but useful diff of expected and received objects for 352 // easier debugging. 353 return fmt.Errorf("Volume check failed [A-expected, B-got]: %s", cmp.Diff(expectedMap, gotMap)) 354 } 355 return nil 356 } 357 358 // CheckClaims compares all expectedClaims with set of claims at the end of the 359 // test and reports differences. 360 func (r *VolumeReactor) CheckClaims(expectedClaims []*v1.PersistentVolumeClaim) error { 361 r.lock.Lock() 362 defer r.lock.Unlock() 363 364 expectedMap := make(map[string]*v1.PersistentVolumeClaim) 365 gotMap := make(map[string]*v1.PersistentVolumeClaim) 366 for _, c := range expectedClaims { 367 // Don't modify the existing object 368 c = c.DeepCopy() 369 c.ResourceVersion = "" 370 expectedMap[c.Name] = c 371 } 372 for _, c := range r.claims { 373 // We must clone the claim because of golang race check - it was 374 // written by the controller without any locks on it. 375 c = c.DeepCopy() 376 c.ResourceVersion = "" 377 gotMap[c.Name] = c 378 } 379 if !reflect.DeepEqual(expectedMap, gotMap) { 380 // Print ugly but useful diff of expected and received objects for 381 // easier debugging. 382 return fmt.Errorf("Claim check failed [A-expected, B-got result]: %s", cmp.Diff(expectedMap, gotMap)) 383 } 384 return nil 385 } 386 387 // PopChange returns one recorded updated object, either *v1.PersistentVolume 388 // or *v1.PersistentVolumeClaim. Returns nil when there are no changes. 389 func (r *VolumeReactor) PopChange(ctx context.Context) interface{} { 390 r.lock.Lock() 391 defer r.lock.Unlock() 392 393 if len(r.changedObjects) == 0 { 394 return nil 395 } 396 397 // For debugging purposes, print the queue 398 logger := klog.FromContext(ctx) 399 for _, obj := range r.changedObjects { 400 switch obj.(type) { 401 case *v1.PersistentVolume: 402 vol, _ := obj.(*v1.PersistentVolume) 403 logger.V(4).Info("Reactor queue", "volumeName", vol.Name) 404 case *v1.PersistentVolumeClaim: 405 claim, _ := obj.(*v1.PersistentVolumeClaim) 406 logger.V(4).Info("Reactor queue", "PVC", klog.KObj(claim)) 407 } 408 } 409 410 // Pop the first item from the queue and return it 411 obj := r.changedObjects[0] 412 r.changedObjects = r.changedObjects[1:] 413 return obj 414 } 415 416 // SyncAll simulates the controller periodic sync of volumes and claim. It 417 // simply adds all these objects to the internal queue of updates. This method 418 // should be used when the test manually calls syncClaim/syncVolume. Test that 419 // use real controller loop (ctrl.Run()) will get periodic sync automatically. 420 func (r *VolumeReactor) SyncAll() { 421 r.lock.Lock() 422 defer r.lock.Unlock() 423 424 for _, c := range r.claims { 425 r.changedObjects = append(r.changedObjects, c) 426 } 427 for _, v := range r.volumes { 428 r.changedObjects = append(r.changedObjects, v) 429 } 430 r.changedSinceLastSync = 0 431 } 432 433 // GetChangeCount returns changes since last sync. 434 func (r *VolumeReactor) GetChangeCount() int { 435 r.lock.Lock() 436 defer r.lock.Unlock() 437 return r.changedSinceLastSync 438 } 439 440 // DeleteVolumeEvent simulates that a volume has been deleted in etcd and 441 // the controller receives 'volume deleted' event. 442 func (r *VolumeReactor) DeleteVolumeEvent(volume *v1.PersistentVolume) { 443 r.lock.Lock() 444 defer r.lock.Unlock() 445 446 // Remove the volume from list of resulting volumes. 447 delete(r.volumes, volume.Name) 448 449 // Generate deletion event. Cloned volume is needed to prevent races (and we 450 // would get a clone from etcd too). 451 if r.fakeVolumeWatch != nil { 452 r.fakeVolumeWatch.Delete(volume.DeepCopy()) 453 } 454 } 455 456 // DeleteClaimEvent simulates that a claim has been deleted in etcd and the 457 // controller receives 'claim deleted' event. 458 func (r *VolumeReactor) DeleteClaimEvent(claim *v1.PersistentVolumeClaim) { 459 r.lock.Lock() 460 defer r.lock.Unlock() 461 462 // Remove the claim from list of resulting claims. 463 delete(r.claims, claim.Name) 464 465 // Generate deletion event. Cloned volume is needed to prevent races (and we 466 // would get a clone from etcd too). 467 if r.fakeClaimWatch != nil { 468 r.fakeClaimWatch.Delete(claim.DeepCopy()) 469 } 470 } 471 472 // AddClaimEvent simulates that a claim has been deleted in etcd and the 473 // controller receives 'claim added' event. 474 func (r *VolumeReactor) AddClaimEvent(claim *v1.PersistentVolumeClaim) { 475 r.lock.Lock() 476 defer r.lock.Unlock() 477 478 r.claims[claim.Name] = claim 479 // Generate event. No cloning is needed, this claim is not stored in the 480 // controller cache yet. 481 if r.fakeClaimWatch != nil { 482 r.fakeClaimWatch.Add(claim) 483 } 484 } 485 486 // AddClaims adds PVCs into VolumeReactor. 487 func (r *VolumeReactor) AddClaims(claims []*v1.PersistentVolumeClaim) { 488 r.lock.Lock() 489 defer r.lock.Unlock() 490 for _, claim := range claims { 491 r.claims[claim.Name] = claim 492 } 493 } 494 495 // AddVolumes adds PVs into VolumeReactor. 496 func (r *VolumeReactor) AddVolumes(volumes []*v1.PersistentVolume) { 497 r.lock.Lock() 498 defer r.lock.Unlock() 499 for _, volume := range volumes { 500 r.volumes[volume.Name] = volume 501 } 502 } 503 504 // AddClaim adds a PVC into VolumeReactor. 505 func (r *VolumeReactor) AddClaim(claim *v1.PersistentVolumeClaim) { 506 r.lock.Lock() 507 defer r.lock.Unlock() 508 r.claims[claim.Name] = claim 509 } 510 511 // AddVolume adds a PV into VolumeReactor. 512 func (r *VolumeReactor) AddVolume(volume *v1.PersistentVolume) { 513 r.lock.Lock() 514 defer r.lock.Unlock() 515 r.volumes[volume.Name] = volume 516 } 517 518 // DeleteVolume deletes a PV by name. 519 func (r *VolumeReactor) DeleteVolume(name string) { 520 r.lock.Lock() 521 defer r.lock.Unlock() 522 delete(r.volumes, name) 523 } 524 525 // AddClaimBoundToVolume adds a PVC and binds it to corresponding PV. 526 func (r *VolumeReactor) AddClaimBoundToVolume(claim *v1.PersistentVolumeClaim) { 527 r.lock.Lock() 528 defer r.lock.Unlock() 529 r.claims[claim.Name] = claim 530 if volume, ok := r.volumes[claim.Spec.VolumeName]; ok { 531 volume.Status.Phase = v1.VolumeBound 532 } 533 } 534 535 // MarkVolumeAvailable marks a PV available by name. 536 func (r *VolumeReactor) MarkVolumeAvailable(name string) { 537 r.lock.Lock() 538 defer r.lock.Unlock() 539 if volume, ok := r.volumes[name]; ok { 540 volume.Spec.ClaimRef = nil 541 volume.Status.Phase = v1.VolumeAvailable 542 volume.Annotations = nil 543 } 544 } 545 546 // NewVolumeReactor creates a volume reactor. 547 func NewVolumeReactor(ctx context.Context, client *fake.Clientset, fakeVolumeWatch, fakeClaimWatch *watch.FakeWatcher, errors []ReactorError) *VolumeReactor { 548 reactor := &VolumeReactor{ 549 volumes: make(map[string]*v1.PersistentVolume), 550 claims: make(map[string]*v1.PersistentVolumeClaim), 551 fakeVolumeWatch: fakeVolumeWatch, 552 fakeClaimWatch: fakeClaimWatch, 553 errors: errors, 554 watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher), 555 } 556 client.AddReactor("create", "persistentvolumes", func(action core.Action) (handled bool, ret runtime.Object, err error) { 557 return reactor.React(ctx, action) 558 }) 559 560 client.AddReactor("create", "persistentvolumeclaims", func(action core.Action) (handled bool, ret runtime.Object, err error) { 561 return reactor.React(ctx, action) 562 }) 563 client.AddReactor("update", "persistentvolumes", func(action core.Action) (handled bool, ret runtime.Object, err error) { 564 return reactor.React(ctx, action) 565 }) 566 client.AddReactor("update", "persistentvolumeclaims", func(action core.Action) (handled bool, ret runtime.Object, err error) { 567 return reactor.React(ctx, action) 568 }) 569 client.AddReactor("get", "persistentvolumes", func(action core.Action) (handled bool, ret runtime.Object, err error) { 570 return reactor.React(ctx, action) 571 }) 572 client.AddReactor("get", "persistentvolumeclaims", func(action core.Action) (handled bool, ret runtime.Object, err error) { 573 return reactor.React(ctx, action) 574 }) 575 client.AddReactor("delete", "persistentvolumes", func(action core.Action) (handled bool, ret runtime.Object, err error) { 576 return reactor.React(ctx, action) 577 }) 578 client.AddReactor("delete", "persistentvolumeclaims", func(action core.Action) (handled bool, ret runtime.Object, err error) { 579 return reactor.React(ctx, action) 580 }) 581 return reactor 582 }