@@ -35,6 +35,7 @@ import (
3535 "k8s.io/client-go/kubernetes"
3636 "k8s.io/client-go/kubernetes/fake"
3737 listersv1 "k8s.io/client-go/listers/core/v1"
38+ k8stesting "k8s.io/client-go/testing"
3839 "k8s.io/client-go/tools/cache"
3940 "k8s.io/client-go/util/retry"
4041 "sigs.k8s.io/controller-runtime/pkg/envtest"
@@ -2440,12 +2441,11 @@ func TestUpdateNodeLabelsForPod_ManagedGate(t *testing.T) {
24402441 })
24412442}
24422443
2443- // TestLabelerWriteCosts pins the API-call budget of the label write path. Reading the
2444- // node from the informer cache instead of the API server is what makes a label change
2445- // cost a single PATCH, and it lets a reconcile with nothing to do cost nothing at all
2446- // — on a large cluster that difference is per node, on every reconcile.
2447- func TestLabelerWriteCosts (t * testing.T ) {
2448- nodeName := "gpu-node"
2444+ // TestUpdateNodeLabels_CachedNode_UsesPatchOnly pins the API-call budget and payload
2445+ // of the label write path.
2446+ func TestUpdateNodeLabels_CachedNode_UsesPatchOnly (t * testing.T ) {
2447+ const nodeName = "gpu-node"
2448+
24492449 clientset := fake .NewSimpleClientset (& corev1.Node {
24502450 ObjectMeta : metav1.ObjectMeta {
24512451 Name : nodeName ,
@@ -2454,50 +2454,66 @@ func TestLabelerWriteCosts(t *testing.T) {
24542454 })
24552455
24562456 labeler , _ := startTransformTestLabeler (t , clientset , devicecounts.Config {})
2457-
2458- // Let startup reconciliation settle, so every write counted below is one this
2459- // test asked for.
24602457 requireCachedLabel (t , labeler , nodeName , KataEnabledLabel , LabelValueFalse )
24612458
2462- t .Run ("a label change costs one patch and nothing else" , func (t * testing.T ) {
2463- // KataEnabledLabel is the labeler's own output, not one of the inputs
2464- // nodeRequiresReconciliation watches, so dropping it cannot kick off a
2465- // reconcile behind this test's back.
2466- node , err := clientset .CoreV1 ().Nodes ().Get (context .Background (), nodeName , metav1.GetOptions {})
2467- require .NoError (t , err )
2468- delete (node .Labels , KataEnabledLabel )
2469- _ , err = clientset .CoreV1 ().Nodes ().Update (context .Background (), node , metav1.UpdateOptions {})
2470- require .NoError (t , err )
2471-
2472- requireCachedLabelAbsent (t , labeler , nodeName , KataEnabledLabel )
2473- clientset .ClearActions ()
2474-
2475- require .NoError (t , labeler .updateNodeLabels (nodeName ))
2459+ tests := []struct {
2460+ name string
2461+ setup func (* testing.T )
2462+ expectedPatch string
2463+ }{
2464+ {
2465+ name : "label change costs one merge patch" ,
2466+ setup : func (t * testing.T ) {
2467+ node , getErr := clientset .CoreV1 ().Nodes ().Get (t .Context (), nodeName , metav1.GetOptions {})
2468+ require .NoError (t , getErr )
2469+ delete (node .Labels , KataEnabledLabel )
2470+ _ , updateErr := clientset .CoreV1 ().Nodes ().Update (t .Context (), node , metav1.UpdateOptions {})
2471+ require .NoError (t , updateErr )
2472+ requireCachedLabelAbsent (t , labeler , nodeName , KataEnabledLabel )
2473+ },
2474+ expectedPatch : fmt .Sprintf (
2475+ `{"metadata":{"labels":{%q:%q}}}` ,
2476+ KataEnabledLabel ,
2477+ LabelValueFalse ,
2478+ ),
2479+ },
2480+ {
2481+ name : "no-op reconcile costs no API call" ,
2482+ setup : func (t * testing.T ) {
2483+ requireCachedLabel (t , labeler , nodeName , KataEnabledLabel , LabelValueFalse )
2484+ },
2485+ },
2486+ }
24762487
2477- assert .Equal (t , 1 , countNodeActions (clientset , "patch" ), "the write itself" )
2478- assert .Equal (t , 0 , countNodeActions (clientset , "get" ), "the node came from the cache" )
2479- assert .Equal (t , 0 , countNodeActions (clientset , "update" ), "PUT is no longer used" )
2488+ for _ , tt := range tests {
2489+ t .Run (tt .name , func (t * testing.T ) {
2490+ tt .setup (t )
2491+ clientset .ClearActions ()
24802492
2481- updated , err := clientset .CoreV1 ().Nodes ().Get (context .Background (), nodeName , metav1.GetOptions {})
2482- require .NoError (t , err )
2483- assert .Equal (t , LabelValueFalse , updated .Labels [KataEnabledLabel ], "the patch landed" )
2484- assert .Equal (t , LabelValueTrue , updated .Labels [gpuPresentLabel ],
2485- "a merge patch must leave labels it does not mention alone" )
2486- })
2493+ require .NoError (t , labeler .updateNodeLabels (nodeName ))
24872494
2488- t .Run ("a reconcile with nothing to do costs no api call" , func (t * testing.T ) {
2489- requireCachedLabel (t , labeler , nodeName , KataEnabledLabel , LabelValueFalse )
2490- clientset .ClearActions ()
2495+ expectedPatchCount := 0
2496+ if tt .expectedPatch != "" {
2497+ expectedPatchCount = 1
2498+ action := nodePatchAction (t , clientset .Actions ())
2499+ assert .Equal (t , types .MergePatchType , action .GetPatchType ())
2500+ assert .JSONEq (t , tt .expectedPatch , string (action .GetPatch ()))
2501+ }
24912502
2492- require .NoError (t , labeler .updateNodeLabels (nodeName ))
2503+ assert .Equal (t , expectedPatchCount , countNodeActions (clientset , "patch" ))
2504+ assert .Equal (t , 0 , countNodeActions (clientset , "get" ))
2505+ assert .Equal (t , 0 , countNodeActions (clientset , "update" ))
24932506
2494- assert .Equal (t , 0 , countNodeActions (clientset , "patch" ))
2495- assert .Equal (t , 0 , countNodeActions (clientset , "get" ))
2496- assert .Equal (t , 0 , countNodeActions (clientset , "update" ))
2497- })
2507+ updated , getErr := clientset .CoreV1 ().Nodes ().Get (t .Context (), nodeName , metav1.GetOptions {})
2508+ require .NoError (t , getErr )
2509+ assert .Equal (t , LabelValueFalse , updated .Labels [KataEnabledLabel ], "the desired label is present" )
2510+ assert .Equal (t , LabelValueTrue , updated .Labels [gpuPresentLabel ],
2511+ "a merge patch must leave labels it does not mention alone" )
2512+ })
2513+ }
24982514}
24992515
2500- // TestLabelsConvergeUnderNodeChurn attacks the assumption the PATCH path rests on:
2516+ // TestLabelsConvergeUnderNodeChurn_FinalInputs_Converge attacks the assumption the PATCH path rests on:
25012517// that reading the node from the informer cache can go stale without ever losing a
25022518// write.
25032519//
@@ -2513,7 +2529,7 @@ func TestLabelerWriteCosts(t *testing.T) {
25132529// The churn below rewrites the input far faster than the cache can follow. Whatever
25142530// the labeler does in the middle, every node has to end up on the label its final
25152531// input implies.
2516- func TestLabelsConvergeUnderNodeChurn (t * testing.T ) {
2532+ func TestLabelsConvergeUnderNodeChurn_FinalInputs_Converge (t * testing.T ) {
25172533 const (
25182534 nodeCount = 20
25192535 rounds = 8
@@ -2613,6 +2629,22 @@ func countNodeActions(clientset *fake.Clientset, verb string) int {
26132629 return count
26142630}
26152631
2632+ func nodePatchAction (t * testing.T , actions []k8stesting.Action ) k8stesting.PatchAction {
2633+ t .Helper ()
2634+
2635+ for _ , action := range actions {
2636+ if action .GetVerb () == "patch" && action .GetResource ().Resource == "nodes" {
2637+ patchAction , ok := action .(k8stesting.PatchAction )
2638+ require .True (t , ok )
2639+
2640+ return patchAction
2641+ }
2642+ }
2643+
2644+ require .FailNow (t , "node PATCH action not found" )
2645+ return nil
2646+ }
2647+
26162648// requireCachedLabel waits for the informer cache to show the given label, which is
26172649// how a test tells that the labeler has finished reacting to earlier events.
26182650func requireCachedLabel (t * testing.T , l * Labeler , nodeName , key , value string ) {
0 commit comments