k8s.io/client-go@v0.22.2/tools/cache/controller_test.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 cache 18 19 import ( 20 "fmt" 21 "math/rand" 22 "sync" 23 "testing" 24 "time" 25 26 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/util/sets" 30 "k8s.io/apimachinery/pkg/util/wait" 31 "k8s.io/apimachinery/pkg/watch" 32 fcache "k8s.io/client-go/tools/cache/testing" 33 34 "github.com/google/gofuzz" 35 ) 36 37 func Example() { 38 // source simulates an apiserver object endpoint. 39 source := fcache.NewFakeControllerSource() 40 41 // This will hold the downstream state, as we know it. 42 downstream := NewStore(DeletionHandlingMetaNamespaceKeyFunc) 43 44 // This will hold incoming changes. Note how we pass downstream in as a 45 // KeyLister, that way resync operations will result in the correct set 46 // of update/delete deltas. 47 fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{ 48 KeyFunction: MetaNamespaceKeyFunc, 49 KnownObjects: downstream, 50 }) 51 52 // Let's do threadsafe output to get predictable test results. 53 deletionCounter := make(chan string, 1000) 54 55 cfg := &Config{ 56 Queue: fifo, 57 ListerWatcher: source, 58 ObjectType: &v1.Pod{}, 59 FullResyncPeriod: time.Millisecond * 100, 60 RetryOnError: false, 61 62 // Let's implement a simple controller that just deletes 63 // everything that comes in. 64 Process: func(obj interface{}) error { 65 // Obj is from the Pop method of the Queue we make above. 66 newest := obj.(Deltas).Newest() 67 68 if newest.Type != Deleted { 69 // Update our downstream store. 70 err := downstream.Add(newest.Object) 71 if err != nil { 72 return err 73 } 74 75 // Delete this object. 76 source.Delete(newest.Object.(runtime.Object)) 77 } else { 78 // Update our downstream store. 79 err := downstream.Delete(newest.Object) 80 if err != nil { 81 return err 82 } 83 84 // fifo's KeyOf is easiest, because it handles 85 // DeletedFinalStateUnknown markers. 86 key, err := fifo.KeyOf(newest.Object) 87 if err != nil { 88 return err 89 } 90 91 // Report this deletion. 92 deletionCounter <- key 93 } 94 return nil 95 }, 96 } 97 98 // Create the controller and run it until we close stop. 99 stop := make(chan struct{}) 100 defer close(stop) 101 go New(cfg).Run(stop) 102 103 // Let's add a few objects to the source. 104 testIDs := []string{"a-hello", "b-controller", "c-framework"} 105 for _, name := range testIDs { 106 // Note that these pods are not valid-- the fake source doesn't 107 // call validation or anything. 108 source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: name}}) 109 } 110 111 // Let's wait for the controller to process the things we just added. 112 outputSet := sets.String{} 113 for i := 0; i < len(testIDs); i++ { 114 outputSet.Insert(<-deletionCounter) 115 } 116 117 for _, key := range outputSet.List() { 118 fmt.Println(key) 119 } 120 // Output: 121 // a-hello 122 // b-controller 123 // c-framework 124 } 125 126 func ExampleNewInformer() { 127 // source simulates an apiserver object endpoint. 128 source := fcache.NewFakeControllerSource() 129 130 // Let's do threadsafe output to get predictable test results. 131 deletionCounter := make(chan string, 1000) 132 133 // Make a controller that immediately deletes anything added to it, and 134 // logs anything deleted. 135 _, controller := NewInformer( 136 source, 137 &v1.Pod{}, 138 time.Millisecond*100, 139 ResourceEventHandlerFuncs{ 140 AddFunc: func(obj interface{}) { 141 source.Delete(obj.(runtime.Object)) 142 }, 143 DeleteFunc: func(obj interface{}) { 144 key, err := DeletionHandlingMetaNamespaceKeyFunc(obj) 145 if err != nil { 146 key = "oops something went wrong with the key" 147 } 148 149 // Report this deletion. 150 deletionCounter <- key 151 }, 152 }, 153 ) 154 155 // Run the controller and run it until we close stop. 156 stop := make(chan struct{}) 157 defer close(stop) 158 go controller.Run(stop) 159 160 // Let's add a few objects to the source. 161 testIDs := []string{"a-hello", "b-controller", "c-framework"} 162 for _, name := range testIDs { 163 // Note that these pods are not valid-- the fake source doesn't 164 // call validation or anything. 165 source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: name}}) 166 } 167 168 // Let's wait for the controller to process the things we just added. 169 outputSet := sets.String{} 170 for i := 0; i < len(testIDs); i++ { 171 outputSet.Insert(<-deletionCounter) 172 } 173 174 for _, key := range outputSet.List() { 175 fmt.Println(key) 176 } 177 // Output: 178 // a-hello 179 // b-controller 180 // c-framework 181 } 182 183 func TestHammerController(t *testing.T) { 184 // This test executes a bunch of requests through the fake source and 185 // controller framework to make sure there's no locking/threading 186 // errors. If an error happens, it should hang forever or trigger the 187 // race detector. 188 189 // source simulates an apiserver object endpoint. 190 source := fcache.NewFakeControllerSource() 191 192 // Let's do threadsafe output to get predictable test results. 193 outputSetLock := sync.Mutex{} 194 // map of key to operations done on the key 195 outputSet := map[string][]string{} 196 197 recordFunc := func(eventType string, obj interface{}) { 198 key, err := DeletionHandlingMetaNamespaceKeyFunc(obj) 199 if err != nil { 200 t.Errorf("something wrong with key: %v", err) 201 key = "oops something went wrong with the key" 202 } 203 204 // Record some output when items are deleted. 205 outputSetLock.Lock() 206 defer outputSetLock.Unlock() 207 outputSet[key] = append(outputSet[key], eventType) 208 } 209 210 // Make a controller which just logs all the changes it gets. 211 _, controller := NewInformer( 212 source, 213 &v1.Pod{}, 214 time.Millisecond*100, 215 ResourceEventHandlerFuncs{ 216 AddFunc: func(obj interface{}) { recordFunc("add", obj) }, 217 UpdateFunc: func(oldObj, newObj interface{}) { recordFunc("update", newObj) }, 218 DeleteFunc: func(obj interface{}) { recordFunc("delete", obj) }, 219 }, 220 ) 221 222 if controller.HasSynced() { 223 t.Errorf("Expected HasSynced() to return false before we started the controller") 224 } 225 226 // Run the controller and run it until we close stop. 227 stop := make(chan struct{}) 228 go controller.Run(stop) 229 230 // Let's wait for the controller to do its initial sync 231 wait.Poll(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { 232 return controller.HasSynced(), nil 233 }) 234 if !controller.HasSynced() { 235 t.Errorf("Expected HasSynced() to return true after the initial sync") 236 } 237 238 wg := sync.WaitGroup{} 239 const threads = 3 240 wg.Add(threads) 241 for i := 0; i < threads; i++ { 242 go func() { 243 defer wg.Done() 244 // Let's add a few objects to the source. 245 currentNames := sets.String{} 246 rs := rand.NewSource(rand.Int63()) 247 f := fuzz.New().NilChance(.5).NumElements(0, 2).RandSource(rs) 248 for i := 0; i < 100; i++ { 249 var name string 250 var isNew bool 251 if currentNames.Len() == 0 || rand.Intn(3) == 1 { 252 f.Fuzz(&name) 253 isNew = true 254 } else { 255 l := currentNames.List() 256 name = l[rand.Intn(len(l))] 257 } 258 259 pod := &v1.Pod{} 260 f.Fuzz(pod) 261 pod.ObjectMeta.Name = name 262 pod.ObjectMeta.Namespace = "default" 263 // Add, update, or delete randomly. 264 // Note that these pods are not valid-- the fake source doesn't 265 // call validation or perform any other checking. 266 if isNew { 267 currentNames.Insert(name) 268 source.Add(pod) 269 continue 270 } 271 switch rand.Intn(2) { 272 case 0: 273 currentNames.Insert(name) 274 source.Modify(pod) 275 case 1: 276 currentNames.Delete(name) 277 source.Delete(pod) 278 } 279 } 280 }() 281 } 282 wg.Wait() 283 284 // Let's wait for the controller to finish processing the things we just added. 285 // TODO: look in the queue to see how many items need to be processed. 286 time.Sleep(100 * time.Millisecond) 287 close(stop) 288 289 // TODO: Verify that no goroutines were leaked here and that everything shut 290 // down cleanly. 291 292 outputSetLock.Lock() 293 t.Logf("got: %#v", outputSet) 294 } 295 296 func TestUpdate(t *testing.T) { 297 // This test is going to exercise the various paths that result in a 298 // call to update. 299 300 // source simulates an apiserver object endpoint. 301 source := fcache.NewFakeControllerSource() 302 303 const ( 304 FROM = "from" 305 TO = "to" 306 ) 307 308 // These are the transitions we expect to see; because this is 309 // asynchronous, there are a lot of valid possibilities. 310 type pair struct{ from, to string } 311 allowedTransitions := map[pair]bool{ 312 {FROM, TO}: true, 313 314 // Because a resync can happen when we've already observed one 315 // of the above but before the item is deleted. 316 {TO, TO}: true, 317 // Because a resync could happen before we observe an update. 318 {FROM, FROM}: true, 319 } 320 321 pod := func(name, check string, final bool) *v1.Pod { 322 p := &v1.Pod{ 323 ObjectMeta: metav1.ObjectMeta{ 324 Name: name, 325 Labels: map[string]string{"check": check}, 326 }, 327 } 328 if final { 329 p.Labels["final"] = "true" 330 } 331 return p 332 } 333 deletePod := func(p *v1.Pod) bool { 334 return p.Labels["final"] == "true" 335 } 336 337 tests := []func(string){ 338 func(name string) { 339 name = "a-" + name 340 source.Add(pod(name, FROM, false)) 341 source.Modify(pod(name, TO, true)) 342 }, 343 } 344 345 const threads = 3 346 347 var testDoneWG sync.WaitGroup 348 testDoneWG.Add(threads * len(tests)) 349 350 // Make a controller that deletes things once it observes an update. 351 // It calls Done() on the wait group on deletions so we can tell when 352 // everything we've added has been deleted. 353 watchCh := make(chan struct{}) 354 _, controller := NewInformer( 355 &testLW{ 356 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 357 watch, err := source.Watch(options) 358 close(watchCh) 359 return watch, err 360 }, 361 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 362 return source.List(options) 363 }, 364 }, 365 &v1.Pod{}, 366 0, 367 ResourceEventHandlerFuncs{ 368 UpdateFunc: func(oldObj, newObj interface{}) { 369 o, n := oldObj.(*v1.Pod), newObj.(*v1.Pod) 370 from, to := o.Labels["check"], n.Labels["check"] 371 if !allowedTransitions[pair{from, to}] { 372 t.Errorf("observed transition %q -> %q for %v", from, to, n.Name) 373 } 374 if deletePod(n) { 375 source.Delete(n) 376 } 377 }, 378 DeleteFunc: func(obj interface{}) { 379 testDoneWG.Done() 380 }, 381 }, 382 ) 383 384 // Run the controller and run it until we close stop. 385 // Once Run() is called, calls to testDoneWG.Done() might start, so 386 // all testDoneWG.Add() calls must happen before this point 387 stop := make(chan struct{}) 388 go controller.Run(stop) 389 <-watchCh 390 391 // run every test a few times, in parallel 392 var wg sync.WaitGroup 393 wg.Add(threads * len(tests)) 394 for i := 0; i < threads; i++ { 395 for j, f := range tests { 396 go func(name string, f func(string)) { 397 defer wg.Done() 398 f(name) 399 }(fmt.Sprintf("%v-%v", i, j), f) 400 } 401 } 402 wg.Wait() 403 404 // Let's wait for the controller to process the things we just added. 405 testDoneWG.Wait() 406 close(stop) 407 } 408 409 func TestPanicPropagated(t *testing.T) { 410 // source simulates an apiserver object endpoint. 411 source := fcache.NewFakeControllerSource() 412 413 // Make a controller that just panic if the AddFunc is called. 414 _, controller := NewInformer( 415 source, 416 &v1.Pod{}, 417 time.Millisecond*100, 418 ResourceEventHandlerFuncs{ 419 AddFunc: func(obj interface{}) { 420 // Create a panic. 421 panic("Just panic.") 422 }, 423 }, 424 ) 425 426 // Run the controller and run it until we close stop. 427 stop := make(chan struct{}) 428 defer close(stop) 429 430 propagated := make(chan interface{}) 431 go func() { 432 defer func() { 433 if r := recover(); r != nil { 434 propagated <- r 435 } 436 }() 437 controller.Run(stop) 438 }() 439 // Let's add a object to the source. It will trigger a panic. 440 source.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) 441 442 // Check if the panic propagated up. 443 select { 444 case p := <-propagated: 445 if p == "Just panic." { 446 t.Logf("Test Passed") 447 } else { 448 t.Errorf("unrecognized panic in controller run: %v", p) 449 } 450 case <-time.After(wait.ForeverTestTimeout): 451 t.Errorf("timeout: the panic failed to propagate from the controller run method!") 452 } 453 }