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 }