@@ -29,15 +29,22 @@ import (
29
29
"github.com/pkg/errors"
30
30
corev1 "k8s.io/api/core/v1"
31
31
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32
+ "k8s.io/apimachinery/pkg/types"
32
33
"k8s.io/utils/pointer"
33
34
"sigs.k8s.io/controller-runtime/pkg/client"
34
35
36
+ clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
37
+ controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
35
38
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
39
+ "sigs.k8s.io/cluster-api/test/e2e/internal/log"
36
40
"sigs.k8s.io/cluster-api/test/framework"
37
41
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
38
42
"sigs.k8s.io/cluster-api/util"
43
+ "sigs.k8s.io/cluster-api/util/conditions"
39
44
)
40
45
46
+ var hookFailedMessage = "hook failed"
47
+
41
48
// clusterUpgradeWithRuntimeSDKSpecInput is the input for clusterUpgradeWithRuntimeSDKSpec.
42
49
type clusterUpgradeWithRuntimeSDKSpecInput struct {
43
50
E2EConfig * clusterctl.E2EConfig
@@ -113,7 +120,7 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
113
120
workerMachineCount = * input .WorkerMachineCount
114
121
}
115
122
116
- // Setup a Namespace where to host objects for this spec and create a watcher for the Namespace events.
123
+ // Set up a Namespace where to host objects for this spec and create a watcher for the Namespace events.
117
124
namespace , cancelWatches = setupSpecNamespace (ctx , specName , input .BootstrapClusterProxy , input .ArtifactFolder )
118
125
clusterResources = new (clusterctl.ApplyClusterTemplateAndWaitResult )
119
126
})
@@ -156,6 +163,12 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
156
163
ControlPlaneMachineCount : pointer .Int64Ptr (controlPlaneMachineCount ),
157
164
WorkerMachineCount : pointer .Int64Ptr (workerMachineCount ),
158
165
},
166
+ PreWaitForCluster : func () {
167
+ beforeClusterCreateTestHandler (ctx ,
168
+ input .BootstrapClusterProxy .GetClient (),
169
+ namespace .Name , clusterName ,
170
+ input .E2EConfig .GetIntervals (specName , "wait-cluster" ))
171
+ },
159
172
WaitForClusterIntervals : input .E2EConfig .GetIntervals (specName , "wait-cluster" ),
160
173
WaitForControlPlaneIntervals : input .E2EConfig .GetIntervals (specName , "wait-control-plane" ),
161
174
WaitForMachineDeployments : input .E2EConfig .GetIntervals (specName , "wait-worker-nodes" ),
@@ -176,6 +189,21 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
176
189
WaitForKubeProxyUpgrade : input .E2EConfig .GetIntervals (specName , "wait-machine-upgrade" ),
177
190
WaitForDNSUpgrade : input .E2EConfig .GetIntervals (specName , "wait-machine-upgrade" ),
178
191
WaitForEtcdUpgrade : input .E2EConfig .GetIntervals (specName , "wait-machine-upgrade" ),
192
+ PreWaitForControlPlaneToBeUpgraded : func () {
193
+ beforeClusterUpgradeTestHandler (ctx ,
194
+ input .BootstrapClusterProxy .GetClient (),
195
+ namespace .Name ,
196
+ clusterName ,
197
+ input .E2EConfig .GetIntervals (specName , "wait-machine-upgrade" ))
198
+ },
199
+ PreWaitForMachineDeploymentToBeUpgraded : func () {
200
+ afterControlPlaneUpgradeTestHandler (ctx ,
201
+ input .BootstrapClusterProxy .GetClient (),
202
+ namespace .Name ,
203
+ clusterName ,
204
+ input .E2EConfig .GetVariable (KubernetesVersionUpgradeTo ),
205
+ input .E2EConfig .GetIntervals (specName , "wait-machine-upgrade" ))
206
+ },
179
207
})
180
208
181
209
// Only attempt to upgrade MachinePools if they were provided in the template.
@@ -201,13 +229,13 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
201
229
})
202
230
203
231
By ("Checking all lifecycle hooks have been called" )
204
- // Assert that each hook passed to this function is marked as "true" in the response configmap
205
- err = checkLifecycleHooks (ctx , input .BootstrapClusterProxy .GetClient (), namespace .Name , clusterName , map [string ]string {
206
- "BeforeClusterCreate" : "" ,
207
- "BeforeClusterUpgrade" : "" ,
208
- "AfterControlPlaneInitialized" : "" ,
209
- "AfterControlPlaneUpgrade" : "" ,
210
- "AfterClusterUpgrade" : "" ,
232
+ // Assert that each hook has been called and returned "Success" during the test.
233
+ err = checkLifecycleHookResponses (ctx , input .BootstrapClusterProxy .GetClient (), namespace .Name , clusterName , map [string ]string {
234
+ "BeforeClusterCreate" : "Success " ,
235
+ "BeforeClusterUpgrade" : "Success " ,
236
+ "AfterControlPlaneInitialized" : "Success " ,
237
+ "AfterControlPlaneUpgrade" : "Success " ,
238
+ "AfterClusterUpgrade" : "Success " ,
211
239
})
212
240
Expect (err ).ToNot (HaveOccurred (), "Lifecycle hook calls were not as expected" )
213
241
@@ -266,26 +294,193 @@ func responsesConfigMap(name string, namespace *corev1.Namespace) *corev1.Config
266
294
Name : fmt .Sprintf ("%s-hookresponses" , name ),
267
295
Namespace : namespace .Name ,
268
296
},
269
- // Every response contain only Status:Success. The test checks whether each handler has been called at least once .
297
+ // Set the initial preloadedResponses for each of the tested hooks .
270
298
Data : map [string ]string {
271
- "BeforeClusterCreate-response" : `{"Status": "Success"}` ,
272
- "BeforeClusterUpgrade-response" : `{"Status": "Success"}` ,
273
- "AfterControlPlaneInitialized-response" : `{"Status": "Success"}` ,
274
- "AfterControlPlaneUpgrade-response" : `{"Status": "Success"}` ,
275
- "AfterClusterUpgrade-response" : `{"Status": "Success"}` ,
299
+ // Blocking hooks are set to Status:Failure initially. These will be changed during the test.
300
+ "BeforeClusterCreate-preloadedResponse" : fmt .Sprintf (`{"Status": "Failure", "Message": %q}` , hookFailedMessage ),
301
+ "BeforeClusterUpgrade-preloadedResponse" : fmt .Sprintf (`{"Status": "Failure", "Message": %q}` , hookFailedMessage ),
302
+ "AfterControlPlaneUpgrade-preloadedResponse" : fmt .Sprintf (`{"Status": "Failure", "Message": %q}` , hookFailedMessage ),
303
+
304
+ // Non-blocking hooks are set to Status:Success.
305
+ "AfterControlPlaneInitialized-preloadedResponse" : `{"Status": "Success"}` ,
306
+ "AfterClusterUpgrade-preloadedResponse" : `{"Status": "Success"}` ,
276
307
},
277
308
}
278
309
}
279
310
280
- func checkLifecycleHooks (ctx context.Context , c client.Client , namespace string , clusterName string , hooks map [string ]string ) error {
281
- configMap := & corev1.ConfigMap {}
282
- configMapName := clusterName + "-hookresponses"
283
- err := c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : configMapName }, configMap )
284
- Expect (err ).ToNot (HaveOccurred (), "Failed to get the hook response configmap" )
285
- for hook := range hooks {
286
- if _ , ok := configMap .Data [hook + "-called" ]; ! ok {
287
- return errors .Errorf ("hook %s call not recorded in configMap %s/%s" , hook , namespace , configMapName )
311
+ // Check that each hook in hooks has been called at least once by checking if its actualResponseStatus is in the hook response configmap.
312
+ // If the provided hooks have both keys and values check that the values match those in the hook response configmap.
313
+ func checkLifecycleHookResponses (ctx context.Context , c client.Client , namespace string , clusterName string , expectedHookResponses map [string ]string ) error {
314
+ responseData := getLifecycleHookResponsesFromConfigMap (ctx , c , namespace , clusterName )
315
+ for hookName , expectedResponse := range expectedHookResponses {
316
+ actualResponse , ok := responseData [hookName + "-actualResponseStatus" ]
317
+ if ! ok {
318
+ return errors .Errorf ("hook %s call not recorded in configMap %s/%s" , hookName , namespace , clusterName + "-hookresponses" )
319
+ }
320
+ if expectedResponse != "" && expectedResponse != actualResponse {
321
+ return errors .Errorf ("hook %s was expected to be %s in configMap got %s" , hookName , expectedResponse , actualResponse )
322
+ }
323
+ }
324
+ return nil
325
+ }
326
+
327
+ // Check that each hook in expectedHooks has been called at least once by checking if its actualResponseStatus is in the hook response configmap.
328
+ func checkLifecycleHooksCalledAtLeastOnce (ctx context.Context , c client.Client , namespace string , clusterName string , expectedHooks []string ) error {
329
+ responseData := getLifecycleHookResponsesFromConfigMap (ctx , c , namespace , clusterName )
330
+ for _ , hookName := range expectedHooks {
331
+ if _ , ok := responseData [hookName + "-actualResponseStatus" ]; ! ok {
332
+ return errors .Errorf ("hook %s call not recorded in configMap %s/%s" , hookName , namespace , clusterName + "-hookresponses" )
288
333
}
289
334
}
290
335
return nil
291
336
}
337
+
338
+ func getLifecycleHookResponsesFromConfigMap (ctx context.Context , c client.Client , namespace string , clusterName string ) map [string ]string {
339
+ configMap := & corev1.ConfigMap {}
340
+ configMapName := clusterName + "-hookresponses"
341
+ Eventually (func () error {
342
+ return c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : configMapName }, configMap )
343
+ }).Should (Succeed (), "Failed to get the hook response configmap" )
344
+ return configMap .Data
345
+ }
346
+
347
+ // beforeClusterCreateTestHandler calls runtimeHookTestHandler with a blockedCondition function which returns false if
348
+ // the Cluster has entered ClusterPhaseProvisioned.
349
+ func beforeClusterCreateTestHandler (ctx context.Context , c client.Client , namespace , clusterName string , intervals []interface {}) {
350
+ log .Logf ("Blocking with BeforeClusterCreate hook" )
351
+ hookName := "BeforeClusterCreate"
352
+ runtimeHookTestHandler (ctx , c , namespace , clusterName , hookName , func () bool {
353
+ blocked := true
354
+ // This hook should block the Cluster from entering the "Provisioned" state.
355
+ cluster := & clusterv1.Cluster {}
356
+ Eventually (func () error {
357
+ return c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : clusterName }, cluster )
358
+ }).Should (Succeed ())
359
+
360
+ // Check if the TopologyReconciled condition message contains both the hook name and hookFailedMessage.
361
+ if ! clusterConditionShowsHookFailed (cluster , hookName ) {
362
+ blocked = false
363
+ }
364
+ if cluster .Status .Phase == string (clusterv1 .ClusterPhaseProvisioned ) {
365
+ blocked = false
366
+ }
367
+ return blocked
368
+ }, intervals )
369
+ }
370
+
371
+ // beforeClusterUpgradeTestHandler calls runtimeHookTestHandler with a blocking function which returns false if the
372
+ // Cluster has controlplanev1.RollingUpdateInProgressReason in its ReadyCondition.
373
+ func beforeClusterUpgradeTestHandler (ctx context.Context , c client.Client , namespace , clusterName string , intervals []interface {}) {
374
+ log .Logf ("Blocking with BeforeClusterUpgrade hook" )
375
+ hookName := "BeforeClusterUpgrade"
376
+ runtimeHookTestHandler (ctx , c , namespace , clusterName , hookName , func () bool {
377
+ var blocked = true
378
+
379
+ cluster := & clusterv1.Cluster {}
380
+ Eventually (func () error {
381
+ return c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : clusterName }, cluster )
382
+ }).Should (Succeed ())
383
+
384
+ // Check if the TopologyReconciled condition message contains both the hook name and hookFailedMessage.
385
+ if ! clusterConditionShowsHookFailed (cluster , hookName ) {
386
+ blocked = false
387
+ }
388
+ // Check if the Cluster is showing the RollingUpdateInProgress condition reason. If it has the update process is unblocked.
389
+ if conditions .IsFalse (cluster , clusterv1 .ReadyCondition ) &&
390
+ conditions .GetReason (cluster , clusterv1 .ReadyCondition ) == controlplanev1 .RollingUpdateInProgressReason {
391
+ blocked = false
392
+ }
393
+ return blocked
394
+ }, intervals )
395
+ }
396
+
397
+ // afterControlPlaneUpgradeTestHandler calls runtimeHookTestHandler with a blocking function which returns false if any
398
+ // MachineDeployment in the Cluster has upgraded to the target Kubernetes version.
399
+ func afterControlPlaneUpgradeTestHandler (ctx context.Context , c client.Client , namespace , clusterName , version string , intervals []interface {}) {
400
+ log .Logf ("Blocking with AfterControlPlaneUpgrade hook" )
401
+ hookName := "AfterControlPlaneUpgrade"
402
+ runtimeHookTestHandler (ctx , c , namespace , clusterName , hookName , func () bool {
403
+ var blocked = true
404
+ cluster := & clusterv1.Cluster {}
405
+ Eventually (func () error {
406
+ return c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : clusterName }, cluster )
407
+ }).Should (Succeed ())
408
+
409
+ // Check if the TopologyReconciled condition message contains both the hook name and hookFailedMessage.
410
+ if ! clusterConditionShowsHookFailed (cluster , hookName ) {
411
+ blocked = false
412
+ }
413
+
414
+ mds := & clusterv1.MachineDeploymentList {}
415
+ Eventually (func () error {
416
+ return c .List (ctx , mds , client.MatchingLabels {
417
+ clusterv1 .ClusterLabelName : clusterName ,
418
+ clusterv1 .ClusterTopologyOwnedLabel : "" ,
419
+ })
420
+ }).Should (Succeed ())
421
+
422
+ // If any of the MachineDeployments have the target Kubernetes Version, the hook is unblocked.
423
+ for _ , md := range mds .Items {
424
+ if * md .Spec .Template .Spec .Version == version {
425
+ blocked = false
426
+ }
427
+ }
428
+ return blocked
429
+ }, intervals )
430
+ }
431
+
432
+ // runtimeHookTestHandler runs a series of tests in sequence to check if the runtimeHook passed to it succeeds.
433
+ // 1) Checks that the hook has been called at least once the TopologyReconciled condition is a Failure.
434
+ // 2) Check that the hook's blockingCondition is consistently true.
435
+ // - At this point the function sets the hook's response to be non-blocking.
436
+ // 3) Check that the hook's blocking condition becomes false.
437
+ // Note: runtimeHookTestHandler assumes that the hook passed to it is currently returning a blocking response.
438
+ // Updating the response to be non-blocking happens inline in the function.
439
+ func runtimeHookTestHandler (ctx context.Context , c client.Client , namespace , clusterName , hookName string , blockingCondition func () bool , intervals []interface {}) {
440
+ // Check that the LifecycleHook has been called at least once and the TopologyReconciled condition is a Failure.
441
+ Eventually (func () error {
442
+ if err := checkLifecycleHooksCalledAtLeastOnce (ctx , c , namespace , clusterName , []string {hookName }); err != nil {
443
+ return err
444
+ }
445
+ cluster := & clusterv1.Cluster {}
446
+ if err := c .Get (ctx , client.ObjectKey {Namespace : namespace , Name : clusterName }, cluster ); err != nil {
447
+ return err
448
+ }
449
+ if ! (conditions .GetReason (cluster , clusterv1 .TopologyReconciledCondition ) == clusterv1 .TopologyReconcileFailedReason ) {
450
+ return errors .New ("Condition not found on Cluster object" )
451
+ }
452
+ return nil
453
+ }, 60 * time .Second ).Should (Succeed (), "%s has not been called" , hookName )
454
+
455
+ // blockingCondition should consistently be true as the Runtime hook is returning "Failure".
456
+ Consistently (func () bool {
457
+ return blockingCondition ()
458
+ }, 30 * time .Second ).Should (BeTrue (),
459
+ fmt .Sprintf ("Cluster Topology reconciliation continued unexpectedly: hook %s not blocking" , hookName ))
460
+
461
+ // Patch the ConfigMap to set the hook response to "Success".
462
+ Byf ("Setting %s response to Status:Success to unblock the reconciliation" , hookName )
463
+
464
+ configMap := & corev1.ConfigMap {ObjectMeta : metav1.ObjectMeta {Name : clusterName + "-hookresponses" , Namespace : namespace }}
465
+ Eventually (func () error {
466
+ return c .Get (ctx , util .ObjectKey (configMap ), configMap )
467
+ }).Should (Succeed ())
468
+ patch := client .RawPatch (types .MergePatchType ,
469
+ []byte (fmt .Sprintf (`{"data":{"%s-preloadedResponse":%s}}` , hookName , "\" {\\ \" Status\\ \" : \\ \" Success\\ \" }\" " )))
470
+ Eventually (func () error {
471
+ return c .Patch (ctx , configMap , patch )
472
+ }).Should (Succeed ())
473
+
474
+ // Expect the Hook to pass, setting the blockingCondition to false before the timeout ends.
475
+ Eventually (func () bool {
476
+ return blockingCondition ()
477
+ }, intervals ... ).Should (BeFalse (),
478
+ fmt .Sprintf ("ClusterTopology reconcile did not unblock after updating hook response: hook %s still blocking" , hookName ))
479
+ }
480
+
481
+ // clusterConditionShowsHookFailed checks if the TopologyReconciled condition message contains both the hook name and hookFailedMessage.
482
+ func clusterConditionShowsHookFailed (cluster * clusterv1.Cluster , hookName string ) bool {
483
+ return conditions .GetReason (cluster , clusterv1 .TopologyReconciledCondition ) == clusterv1 .TopologyReconcileFailedReason &&
484
+ strings .Contains (conditions .GetMessage (cluster , clusterv1 .TopologyReconciledCondition ), hookFailedMessage ) &&
485
+ strings .Contains (conditions .GetMessage (cluster , clusterv1 .TopologyReconciledCondition ), hookName )
486
+ }
0 commit comments