k8s.io/client-go@v0.22.2/testing/fixture.go (about)

     1  /*
     2  Copyright 2015 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  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  	"sync"
    25  
    26  	jsonpatch "github.com/evanphx/json-patch"
    27  
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"k8s.io/apimachinery/pkg/util/json"
    35  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    36  	"k8s.io/apimachinery/pkg/watch"
    37  	restclient "k8s.io/client-go/rest"
    38  )
    39  
    40  // ObjectTracker keeps track of objects. It is intended to be used to
    41  // fake calls to a server by returning objects based on their kind,
    42  // namespace and name.
    43  type ObjectTracker interface {
    44  	// Add adds an object to the tracker. If object being added
    45  	// is a list, its items are added separately.
    46  	Add(obj runtime.Object) error
    47  
    48  	// Get retrieves the object by its kind, namespace and name.
    49  	Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error)
    50  
    51  	// Create adds an object to the tracker in the specified namespace.
    52  	Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error
    53  
    54  	// Update updates an existing object in the tracker in the specified namespace.
    55  	Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error
    56  
    57  	// List retrieves all objects of a given kind in the given
    58  	// namespace. Only non-List kinds are accepted.
    59  	List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error)
    60  
    61  	// Delete deletes an existing object from the tracker. If object
    62  	// didn't exist in the tracker prior to deletion, Delete returns
    63  	// no error.
    64  	Delete(gvr schema.GroupVersionResource, ns, name string) error
    65  
    66  	// Watch watches objects from the tracker. Watch returns a channel
    67  	// which will push added / modified / deleted object.
    68  	Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error)
    69  }
    70  
    71  // ObjectScheme abstracts the implementation of common operations on objects.
    72  type ObjectScheme interface {
    73  	runtime.ObjectCreater
    74  	runtime.ObjectTyper
    75  }
    76  
    77  // ObjectReaction returns a ReactionFunc that applies core.Action to
    78  // the given tracker.
    79  func ObjectReaction(tracker ObjectTracker) ReactionFunc {
    80  	return func(action Action) (bool, runtime.Object, error) {
    81  		ns := action.GetNamespace()
    82  		gvr := action.GetResource()
    83  		// Here and below we need to switch on implementation types,
    84  		// not on interfaces, as some interfaces are identical
    85  		// (e.g. UpdateAction and CreateAction), so if we use them,
    86  		// updates and creates end up matching the same case branch.
    87  		switch action := action.(type) {
    88  
    89  		case ListActionImpl:
    90  			obj, err := tracker.List(gvr, action.GetKind(), ns)
    91  			return true, obj, err
    92  
    93  		case GetActionImpl:
    94  			obj, err := tracker.Get(gvr, ns, action.GetName())
    95  			return true, obj, err
    96  
    97  		case CreateActionImpl:
    98  			objMeta, err := meta.Accessor(action.GetObject())
    99  			if err != nil {
   100  				return true, nil, err
   101  			}
   102  			if action.GetSubresource() == "" {
   103  				err = tracker.Create(gvr, action.GetObject(), ns)
   104  			} else {
   105  				// TODO: Currently we're handling subresource creation as an update
   106  				// on the enclosing resource. This works for some subresources but
   107  				// might not be generic enough.
   108  				err = tracker.Update(gvr, action.GetObject(), ns)
   109  			}
   110  			if err != nil {
   111  				return true, nil, err
   112  			}
   113  			obj, err := tracker.Get(gvr, ns, objMeta.GetName())
   114  			return true, obj, err
   115  
   116  		case UpdateActionImpl:
   117  			objMeta, err := meta.Accessor(action.GetObject())
   118  			if err != nil {
   119  				return true, nil, err
   120  			}
   121  			err = tracker.Update(gvr, action.GetObject(), ns)
   122  			if err != nil {
   123  				return true, nil, err
   124  			}
   125  			obj, err := tracker.Get(gvr, ns, objMeta.GetName())
   126  			return true, obj, err
   127  
   128  		case DeleteActionImpl:
   129  			err := tracker.Delete(gvr, ns, action.GetName())
   130  			if err != nil {
   131  				return true, nil, err
   132  			}
   133  			return true, nil, nil
   134  
   135  		case PatchActionImpl:
   136  			obj, err := tracker.Get(gvr, ns, action.GetName())
   137  			if err != nil {
   138  				return true, nil, err
   139  			}
   140  
   141  			old, err := json.Marshal(obj)
   142  			if err != nil {
   143  				return true, nil, err
   144  			}
   145  
   146  			// reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields
   147  			// in obj that are removed by patch are cleared
   148  			value := reflect.ValueOf(obj)
   149  			value.Elem().Set(reflect.New(value.Type().Elem()).Elem())
   150  
   151  			switch action.GetPatchType() {
   152  			case types.JSONPatchType:
   153  				patch, err := jsonpatch.DecodePatch(action.GetPatch())
   154  				if err != nil {
   155  					return true, nil, err
   156  				}
   157  				modified, err := patch.Apply(old)
   158  				if err != nil {
   159  					return true, nil, err
   160  				}
   161  
   162  				if err = json.Unmarshal(modified, obj); err != nil {
   163  					return true, nil, err
   164  				}
   165  			case types.MergePatchType:
   166  				modified, err := jsonpatch.MergePatch(old, action.GetPatch())
   167  				if err != nil {
   168  					return true, nil, err
   169  				}
   170  
   171  				if err := json.Unmarshal(modified, obj); err != nil {
   172  					return true, nil, err
   173  				}
   174  			case types.StrategicMergePatchType:
   175  				mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj)
   176  				if err != nil {
   177  					return true, nil, err
   178  				}
   179  				if err = json.Unmarshal(mergedByte, obj); err != nil {
   180  					return true, nil, err
   181  				}
   182  			default:
   183  				return true, nil, fmt.Errorf("PatchType is not supported")
   184  			}
   185  
   186  			if err = tracker.Update(gvr, obj, ns); err != nil {
   187  				return true, nil, err
   188  			}
   189  
   190  			return true, obj, nil
   191  
   192  		default:
   193  			return false, nil, fmt.Errorf("no reaction implemented for %s", action)
   194  		}
   195  	}
   196  }
   197  
   198  type tracker struct {
   199  	scheme  ObjectScheme
   200  	decoder runtime.Decoder
   201  	lock    sync.RWMutex
   202  	objects map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object
   203  	// The value type of watchers is a map of which the key is either a namespace or
   204  	// all/non namespace aka "" and its value is list of fake watchers.
   205  	// Manipulations on resources will broadcast the notification events into the
   206  	// watchers' channel. Note that too many unhandled events (currently 100,
   207  	// see apimachinery/pkg/watch.DefaultChanSize) will cause a panic.
   208  	watchers map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher
   209  }
   210  
   211  var _ ObjectTracker = &tracker{}
   212  
   213  // NewObjectTracker returns an ObjectTracker that can be used to keep track
   214  // of objects for the fake clientset. Mostly useful for unit tests.
   215  func NewObjectTracker(scheme ObjectScheme, decoder runtime.Decoder) ObjectTracker {
   216  	return &tracker{
   217  		scheme:   scheme,
   218  		decoder:  decoder,
   219  		objects:  make(map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object),
   220  		watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher),
   221  	}
   222  }
   223  
   224  func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) {
   225  	// Heuristic for list kind: original kind + List suffix. Might
   226  	// not always be true but this tracker has a pretty limited
   227  	// understanding of the actual API model.
   228  	listGVK := gvk
   229  	listGVK.Kind = listGVK.Kind + "List"
   230  	// GVK does have the concept of "internal version". The scheme recognizes
   231  	// the runtime.APIVersionInternal, but not the empty string.
   232  	if listGVK.Version == "" {
   233  		listGVK.Version = runtime.APIVersionInternal
   234  	}
   235  
   236  	list, err := t.scheme.New(listGVK)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	if !meta.IsListType(list) {
   242  		return nil, fmt.Errorf("%q is not a list type", listGVK.Kind)
   243  	}
   244  
   245  	t.lock.RLock()
   246  	defer t.lock.RUnlock()
   247  
   248  	objs, ok := t.objects[gvr]
   249  	if !ok {
   250  		return list, nil
   251  	}
   252  
   253  	matchingObjs, err := filterByNamespace(objs, ns)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	if err := meta.SetList(list, matchingObjs); err != nil {
   258  		return nil, err
   259  	}
   260  	return list.DeepCopyObject(), nil
   261  }
   262  
   263  func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) {
   264  	t.lock.Lock()
   265  	defer t.lock.Unlock()
   266  
   267  	fakewatcher := watch.NewRaceFreeFake()
   268  
   269  	if _, exists := t.watchers[gvr]; !exists {
   270  		t.watchers[gvr] = make(map[string][]*watch.RaceFreeFakeWatcher)
   271  	}
   272  	t.watchers[gvr][ns] = append(t.watchers[gvr][ns], fakewatcher)
   273  	return fakewatcher, nil
   274  }
   275  
   276  func (t *tracker) Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) {
   277  	errNotFound := errors.NewNotFound(gvr.GroupResource(), name)
   278  
   279  	t.lock.RLock()
   280  	defer t.lock.RUnlock()
   281  
   282  	objs, ok := t.objects[gvr]
   283  	if !ok {
   284  		return nil, errNotFound
   285  	}
   286  
   287  	matchingObj, ok := objs[types.NamespacedName{Namespace: ns, Name: name}]
   288  	if !ok {
   289  		return nil, errNotFound
   290  	}
   291  
   292  	// Only one object should match in the tracker if it works
   293  	// correctly, as Add/Update methods enforce kind/namespace/name
   294  	// uniqueness.
   295  	obj := matchingObj.DeepCopyObject()
   296  	if status, ok := obj.(*metav1.Status); ok {
   297  		if status.Status != metav1.StatusSuccess {
   298  			return nil, &errors.StatusError{ErrStatus: *status}
   299  		}
   300  	}
   301  
   302  	return obj, nil
   303  }
   304  
   305  func (t *tracker) Add(obj runtime.Object) error {
   306  	if meta.IsListType(obj) {
   307  		return t.addList(obj, false)
   308  	}
   309  	objMeta, err := meta.Accessor(obj)
   310  	if err != nil {
   311  		return err
   312  	}
   313  	gvks, _, err := t.scheme.ObjectKinds(obj)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	if partial, ok := obj.(*metav1.PartialObjectMetadata); ok && len(partial.TypeMeta.APIVersion) > 0 {
   319  		gvks = []schema.GroupVersionKind{partial.TypeMeta.GroupVersionKind()}
   320  	}
   321  
   322  	if len(gvks) == 0 {
   323  		return fmt.Errorf("no registered kinds for %v", obj)
   324  	}
   325  	for _, gvk := range gvks {
   326  		// NOTE: UnsafeGuessKindToResource is a heuristic and default match. The
   327  		// actual registration in apiserver can specify arbitrary route for a
   328  		// gvk. If a test uses such objects, it cannot preset the tracker with
   329  		// objects via Add(). Instead, it should trigger the Create() function
   330  		// of the tracker, where an arbitrary gvr can be specified.
   331  		gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   332  		// Resource doesn't have the concept of "__internal" version, just set it to "".
   333  		if gvr.Version == runtime.APIVersionInternal {
   334  			gvr.Version = ""
   335  		}
   336  
   337  		err := t.add(gvr, obj, objMeta.GetNamespace(), false)
   338  		if err != nil {
   339  			return err
   340  		}
   341  	}
   342  	return nil
   343  }
   344  
   345  func (t *tracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
   346  	return t.add(gvr, obj, ns, false)
   347  }
   348  
   349  func (t *tracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
   350  	return t.add(gvr, obj, ns, true)
   351  }
   352  
   353  func (t *tracker) getWatches(gvr schema.GroupVersionResource, ns string) []*watch.RaceFreeFakeWatcher {
   354  	watches := []*watch.RaceFreeFakeWatcher{}
   355  	if t.watchers[gvr] != nil {
   356  		if w := t.watchers[gvr][ns]; w != nil {
   357  			watches = append(watches, w...)
   358  		}
   359  		if ns != metav1.NamespaceAll {
   360  			if w := t.watchers[gvr][metav1.NamespaceAll]; w != nil {
   361  				watches = append(watches, w...)
   362  			}
   363  		}
   364  	}
   365  	return watches
   366  }
   367  
   368  func (t *tracker) add(gvr schema.GroupVersionResource, obj runtime.Object, ns string, replaceExisting bool) error {
   369  	t.lock.Lock()
   370  	defer t.lock.Unlock()
   371  
   372  	gr := gvr.GroupResource()
   373  
   374  	// To avoid the object from being accidentally modified by caller
   375  	// after it's been added to the tracker, we always store the deep
   376  	// copy.
   377  	obj = obj.DeepCopyObject()
   378  
   379  	newMeta, err := meta.Accessor(obj)
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	// Propagate namespace to the new object if hasn't already been set.
   385  	if len(newMeta.GetNamespace()) == 0 {
   386  		newMeta.SetNamespace(ns)
   387  	}
   388  
   389  	if ns != newMeta.GetNamespace() {
   390  		msg := fmt.Sprintf("request namespace does not match object namespace, request: %q object: %q", ns, newMeta.GetNamespace())
   391  		return errors.NewBadRequest(msg)
   392  	}
   393  
   394  	_, ok := t.objects[gvr]
   395  	if !ok {
   396  		t.objects[gvr] = make(map[types.NamespacedName]runtime.Object)
   397  	}
   398  
   399  	namespacedName := types.NamespacedName{Namespace: newMeta.GetNamespace(), Name: newMeta.GetName()}
   400  	if _, ok = t.objects[gvr][namespacedName]; ok {
   401  		if replaceExisting {
   402  			for _, w := range t.getWatches(gvr, ns) {
   403  				// To avoid the object from being accidentally modified by watcher
   404  				w.Modify(obj.DeepCopyObject())
   405  			}
   406  			t.objects[gvr][namespacedName] = obj
   407  			return nil
   408  		}
   409  		return errors.NewAlreadyExists(gr, newMeta.GetName())
   410  	}
   411  
   412  	if replaceExisting {
   413  		// Tried to update but no matching object was found.
   414  		return errors.NewNotFound(gr, newMeta.GetName())
   415  	}
   416  
   417  	t.objects[gvr][namespacedName] = obj
   418  
   419  	for _, w := range t.getWatches(gvr, ns) {
   420  		// To avoid the object from being accidentally modified by watcher
   421  		w.Add(obj.DeepCopyObject())
   422  	}
   423  
   424  	return nil
   425  }
   426  
   427  func (t *tracker) addList(obj runtime.Object, replaceExisting bool) error {
   428  	list, err := meta.ExtractList(obj)
   429  	if err != nil {
   430  		return err
   431  	}
   432  	errs := runtime.DecodeList(list, t.decoder)
   433  	if len(errs) > 0 {
   434  		return errs[0]
   435  	}
   436  	for _, obj := range list {
   437  		if err := t.Add(obj); err != nil {
   438  			return err
   439  		}
   440  	}
   441  	return nil
   442  }
   443  
   444  func (t *tracker) Delete(gvr schema.GroupVersionResource, ns, name string) error {
   445  	t.lock.Lock()
   446  	defer t.lock.Unlock()
   447  
   448  	objs, ok := t.objects[gvr]
   449  	if !ok {
   450  		return errors.NewNotFound(gvr.GroupResource(), name)
   451  	}
   452  
   453  	namespacedName := types.NamespacedName{Namespace: ns, Name: name}
   454  	obj, ok := objs[namespacedName]
   455  	if !ok {
   456  		return errors.NewNotFound(gvr.GroupResource(), name)
   457  	}
   458  
   459  	delete(objs, namespacedName)
   460  	for _, w := range t.getWatches(gvr, ns) {
   461  		w.Delete(obj.DeepCopyObject())
   462  	}
   463  	return nil
   464  }
   465  
   466  // filterByNamespace returns all objects in the collection that
   467  // match provided namespace. Empty namespace matches
   468  // non-namespaced objects.
   469  func filterByNamespace(objs map[types.NamespacedName]runtime.Object, ns string) ([]runtime.Object, error) {
   470  	var res []runtime.Object
   471  
   472  	for _, obj := range objs {
   473  		acc, err := meta.Accessor(obj)
   474  		if err != nil {
   475  			return nil, err
   476  		}
   477  		if ns != "" && acc.GetNamespace() != ns {
   478  			continue
   479  		}
   480  		res = append(res, obj)
   481  	}
   482  
   483  	// Sort res to get deterministic order.
   484  	sort.Slice(res, func(i, j int) bool {
   485  		acc1, _ := meta.Accessor(res[i])
   486  		acc2, _ := meta.Accessor(res[j])
   487  		if acc1.GetNamespace() != acc2.GetNamespace() {
   488  			return acc1.GetNamespace() < acc2.GetNamespace()
   489  		}
   490  		return acc1.GetName() < acc2.GetName()
   491  	})
   492  	return res, nil
   493  }
   494  
   495  func DefaultWatchReactor(watchInterface watch.Interface, err error) WatchReactionFunc {
   496  	return func(action Action) (bool, watch.Interface, error) {
   497  		return true, watchInterface, err
   498  	}
   499  }
   500  
   501  // SimpleReactor is a Reactor.  Each reaction function is attached to a given verb,resource tuple.  "*" in either field matches everything for that value.
   502  // For instance, *,pods matches all verbs on pods.  This allows for easier composition of reaction functions
   503  type SimpleReactor struct {
   504  	Verb     string
   505  	Resource string
   506  
   507  	Reaction ReactionFunc
   508  }
   509  
   510  func (r *SimpleReactor) Handles(action Action) bool {
   511  	verbCovers := r.Verb == "*" || r.Verb == action.GetVerb()
   512  	if !verbCovers {
   513  		return false
   514  	}
   515  
   516  	return resourceCovers(r.Resource, action)
   517  }
   518  
   519  func (r *SimpleReactor) React(action Action) (bool, runtime.Object, error) {
   520  	return r.Reaction(action)
   521  }
   522  
   523  // SimpleWatchReactor is a WatchReactor.  Each reaction function is attached to a given resource.  "*" matches everything for that value.
   524  // For instance, *,pods matches all verbs on pods.  This allows for easier composition of reaction functions
   525  type SimpleWatchReactor struct {
   526  	Resource string
   527  
   528  	Reaction WatchReactionFunc
   529  }
   530  
   531  func (r *SimpleWatchReactor) Handles(action Action) bool {
   532  	return resourceCovers(r.Resource, action)
   533  }
   534  
   535  func (r *SimpleWatchReactor) React(action Action) (bool, watch.Interface, error) {
   536  	return r.Reaction(action)
   537  }
   538  
   539  // SimpleProxyReactor is a ProxyReactor.  Each reaction function is attached to a given resource.  "*" matches everything for that value.
   540  // For instance, *,pods matches all verbs on pods.  This allows for easier composition of reaction functions.
   541  type SimpleProxyReactor struct {
   542  	Resource string
   543  
   544  	Reaction ProxyReactionFunc
   545  }
   546  
   547  func (r *SimpleProxyReactor) Handles(action Action) bool {
   548  	return resourceCovers(r.Resource, action)
   549  }
   550  
   551  func (r *SimpleProxyReactor) React(action Action) (bool, restclient.ResponseWrapper, error) {
   552  	return r.Reaction(action)
   553  }
   554  
   555  func resourceCovers(resource string, action Action) bool {
   556  	if resource == "*" {
   557  		return true
   558  	}
   559  
   560  	if resource == action.GetResource().Resource {
   561  		return true
   562  	}
   563  
   564  	if index := strings.Index(resource, "/"); index != -1 &&
   565  		resource[:index] == action.GetResource().Resource &&
   566  		resource[index+1:] == action.GetSubresource() {
   567  		return true
   568  	}
   569  
   570  	return false
   571  }