knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/reconciler/testing/hooks.go (about) 1 /* 2 Copyright 2019 The Knative 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 includes utilities for testing controllers. 18 package testing 19 20 import ( 21 "errors" 22 "sync" 23 "sync/atomic" 24 "time" 25 26 "k8s.io/apimachinery/pkg/runtime" 27 kubetesting "k8s.io/client-go/testing" 28 ) 29 30 // HookResult is the return value of hook functions. 31 type HookResult bool 32 33 const ( 34 // HookComplete indicates the hook function completed, and WaitForHooks should 35 // not wait for it. 36 HookComplete HookResult = true 37 // HookIncomplete indicates the hook function is incomplete, and WaitForHooks 38 // should wait for it to complete. 39 HookIncomplete HookResult = false 40 ) 41 42 /* 43 CreateHookFunc is a function for handling a Create hook. Its runtime.Object 44 parameter will be the Kubernetes resource created. The resource can be cast 45 to its actual type like this: 46 47 pod := obj.(*v1.Pod) 48 49 A return value of true marks the hook as completed. Returning false allows 50 the hook to run again when the next resource of the requested type is 51 created. 52 */ 53 type CreateHookFunc func(runtime.Object) HookResult 54 55 /* 56 UpdateHookFunc is a function for handling an update hook. its runtime.Object 57 parameter will be the Kubernetes resource updated. The resource can be cast 58 to its actual type like this: 59 60 pod := obj.(*v1.Pod) 61 62 A return value of true marks the hook as completed. Returning false allows 63 the hook to run again when the next resource of the requested type is 64 updated. 65 */ 66 type UpdateHookFunc func(runtime.Object) HookResult 67 68 /* 69 DeleteHookFunc is a function for handling a delete hook. Its name parameter will 70 be the name of the resource deleted. The resource itself is not available to 71 the reactor. 72 */ 73 type DeleteHookFunc func(string) HookResult 74 75 /* 76 Hooks is a utility struct that simplifies controller testing with fake 77 clients. A Hooks struct allows attaching hook functions to actions (create, 78 update, delete) on a specified resource type within a fake client and ensuring 79 that all hooks complete in a timely manner. 80 */ 81 type Hooks struct { 82 completionCh chan int32 83 completionIndex *atomic.Int32 84 85 // Denotes whether or not the registered hooks should no longer be called 86 // because they have already been waited upon. 87 // This uses a Mutex over a channel to guarantee that after WaitForHooks 88 // returns no hooked functions will be called. 89 closed bool 90 mutex sync.RWMutex 91 } 92 93 // NewHooks returns a Hooks struct that can be used to attach hooks to one or 94 // more fake clients and wait for all hooks to complete. 95 // TODO(grantr): Allow validating that a hook never fires 96 func NewHooks() *Hooks { 97 var ci atomic.Int32 98 ci.Store(-1) 99 return &Hooks{ 100 completionCh: make(chan int32, 100), 101 completionIndex: &ci, 102 } 103 } 104 105 // OnCreate attaches a create hook to the given Fake. The hook function is 106 // executed every time a resource of the given type is created. 107 func (h *Hooks) OnCreate(fake *kubetesting.Fake, resource string, rf CreateHookFunc) { 108 index := h.completionIndex.Add(1) 109 fake.PrependReactor("create", resource, func(a kubetesting.Action) (bool, runtime.Object, error) { 110 obj := a.(kubetesting.CreateActionImpl).Object 111 112 h.mutex.RLock() 113 defer h.mutex.RUnlock() 114 if !h.closed && rf(obj) == HookComplete { 115 h.completionCh <- index 116 } 117 return false, nil, nil 118 }) 119 } 120 121 // OnUpdate attaches an update hook to the given Fake. The hook function is 122 // executed every time a resource of the given type is updated. 123 func (h *Hooks) OnUpdate(fake *kubetesting.Fake, resource string, rf UpdateHookFunc) { 124 index := h.completionIndex.Add(1) 125 fake.PrependReactor("update", resource, func(a kubetesting.Action) (bool, runtime.Object, error) { 126 obj := a.(kubetesting.UpdateActionImpl).Object 127 128 h.mutex.RLock() 129 defer h.mutex.RUnlock() 130 if !h.closed && rf(obj) == HookComplete { 131 h.completionCh <- index 132 } 133 return false, nil, nil 134 }) 135 } 136 137 // OnDelete attaches a delete hook to the given Fake. The hook function is 138 // executed every time a resource of the given type is deleted. 139 func (h *Hooks) OnDelete(fake *kubetesting.Fake, resource string, rf DeleteHookFunc) { 140 index := h.completionIndex.Add(1) 141 fake.PrependReactor("delete", resource, func(a kubetesting.Action) (bool, runtime.Object, error) { 142 name := a.(kubetesting.DeleteActionImpl).Name 143 144 h.mutex.RLock() 145 defer h.mutex.RUnlock() 146 if !h.closed && rf(name) == HookComplete { 147 h.completionCh <- index 148 } 149 return false, nil, nil 150 }) 151 } 152 153 // WaitForHooks waits until all attached hooks have returned true at least once. 154 // If the given timeout expires before that happens, an error is returned. 155 // The registered actions will no longer be executed after WaitForHooks has 156 // returned. 157 func (h *Hooks) WaitForHooks(timeout time.Duration) error { 158 defer func() { 159 h.mutex.Lock() 160 defer h.mutex.Unlock() 161 h.closed = true 162 }() 163 164 ci := int(h.completionIndex.Load()) 165 if ci == -1 { 166 return nil 167 } 168 169 // Convert index to count. 170 ci++ 171 timer := time.After(timeout) 172 hookCompletions := map[int32]HookResult{} 173 for { 174 select { 175 case i := <-h.completionCh: 176 hookCompletions[i] = HookComplete 177 if len(hookCompletions) == ci { 178 h.completionIndex.Add(-1) 179 return nil 180 } 181 case <-timer: 182 return errors.New("timed out waiting for hooks to complete") 183 } 184 } 185 }