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  }