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  }