Skip to content

Commit d076660

Browse files
killianmuldoonykakarap
and
ykakarap
committed
Add lifecycle hook handlers to test extension
Signed-off-by: killianmuldoon <[email protected]> Co-authored-by: ykakarap <[email protected]>
1 parent 8d7f010 commit d076660

10 files changed

+334
-4
lines changed

test/e2e/cluster_upgrade_runtimesdk.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626

2727
. "github.com/onsi/ginkgo"
2828
. "github.com/onsi/gomega"
29+
"github.com/pkg/errors"
2930
corev1 "k8s.io/api/core/v1"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3132
"k8s.io/utils/pointer"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
3234

3335
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
3436
"sigs.k8s.io/cluster-api/test/framework"
@@ -117,22 +119,26 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
117119
})
118120

119121
It("Should create and upgrade a workload cluster", func() {
122+
clusterName := fmt.Sprintf("%s-%s", specName, util.RandomString(6))
120123
By("Deploy Test Extension")
121124
testExtensionDeploymentTemplate, err := os.ReadFile(testExtensionPath) //nolint:gosec
122-
Expect(err).ToNot(HaveOccurred(), "Failed to read the extension config deployment manifest file")
125+
Expect(err).ToNot(HaveOccurred(), "Failed to read the extension deployment manifest file")
123126

124127
// Set the SERVICE_NAMESPACE, which is used in the cert-manager Certificate CR.
125128
// We have to dynamically set the namespace here, because it depends on the test run and thus
126129
// cannot be set when rendering the test extension YAML with kustomize.
127130
testExtensionDeployment := strings.ReplaceAll(string(testExtensionDeploymentTemplate), "${SERVICE_NAMESPACE}", namespace.Name)
128-
Expect(testExtensionDeployment).ToNot(BeEmpty(), "Test Extension deployment manifest file should not be empty")
129131

132+
Expect(testExtensionDeployment).ToNot(BeEmpty(), "Test Extension deployment manifest file should not be empty")
130133
Expect(input.BootstrapClusterProxy.Apply(ctx, []byte(testExtensionDeployment), "--namespace", namespace.Name)).To(Succeed())
131134

132-
By("Deploy Test Extension ExtensionConfig")
135+
By("Deploy Test Extension ExtensionConfig and ConfigMap")
133136
ext = extensionConfig(specName, namespace)
134137
err = input.BootstrapClusterProxy.GetClient().Create(ctx, ext)
135138
Expect(err).ToNot(HaveOccurred(), "Failed to create the extension config")
139+
responses := responsesConfigMap(clusterName, namespace)
140+
err = input.BootstrapClusterProxy.GetClient().Create(ctx, responses)
141+
Expect(err).ToNot(HaveOccurred(), "Failed to create the responses configmap")
136142

137143
By("Creating a workload cluster")
138144

@@ -145,7 +151,7 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
145151
InfrastructureProvider: clusterctl.DefaultInfrastructureProvider,
146152
Flavor: pointer.StringDeref(input.Flavor, "upgrades"),
147153
Namespace: namespace.Name,
148-
ClusterName: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
154+
ClusterName: clusterName,
149155
KubernetesVersion: input.E2EConfig.GetVariable(KubernetesVersionUpgradeFrom),
150156
ControlPlaneMachineCount: pointer.Int64Ptr(controlPlaneMachineCount),
151157
WorkerMachineCount: pointer.Int64Ptr(workerMachineCount),
@@ -194,6 +200,17 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
194200
WaitForNodesReady: input.E2EConfig.GetIntervals(specName, "wait-nodes-ready"),
195201
})
196202

203+
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": "",
211+
})
212+
Expect(err).ToNot(HaveOccurred(), "Lifecycle hook calls were not as expected")
213+
197214
By("PASSED!")
198215
})
199216

@@ -241,3 +258,34 @@ func extensionConfig(specName string, namespace *corev1.Namespace) *runtimev1.Ex
241258
},
242259
}
243260
}
261+
262+
// responsesConfigMap generates a ConfigMap with preloaded responses for the test extension.
263+
func responsesConfigMap(name string, namespace *corev1.Namespace) *corev1.ConfigMap {
264+
return &corev1.ConfigMap{
265+
ObjectMeta: metav1.ObjectMeta{
266+
Name: fmt.Sprintf("%s-hookresponses", name),
267+
Namespace: namespace.Name,
268+
},
269+
// Every response contain only Status:Success. The test checks whether each handler has been called at least once.
270+
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\"}",
276+
},
277+
}
278+
}
279+
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)
288+
}
289+
}
290+
return nil
291+
}

test/extension/config/default/extension.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ spec:
1919
image: controller:latest
2020
name: extension
2121
terminationGracePeriodSeconds: 10
22+
serviceAccountName: test-extension
2223
tolerations:
2324
- effect: NoSchedule
2425
key: node-role.kubernetes.io/master
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: test-extension
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- image: gcr.io/k8s-staging-cluster-api/test-extension:main
10+
name: extension
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: test-extension
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- name: extension
10+
imagePullPolicy: Always

test/extension/config/default/kustomization.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ commonLabels:
33
resources:
44
- extension.yaml
55
- service.yaml
6+
- role.yaml
7+
- rolebinding.yaml
8+
- service_account.yaml
69

710
bases:
811
- ../certmanager
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: Role
3+
metadata:
4+
name: test-extension
5+
rules:
6+
- apiGroups:
7+
- ""
8+
resources:
9+
- configmaps
10+
verbs:
11+
- get
12+
- list
13+
- watch
14+
- patch
15+
- update
16+
- create
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: RoleBinding
3+
metadata:
4+
name: test-extension
5+
roleRef:
6+
apiGroup: rbac.authorization.k8s.io
7+
kind: Role
8+
name: test-extension
9+
subjects:
10+
- kind: ServiceAccount
11+
name: test-extension
12+
namespace: ${SERVICE_NAMESPACE}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
apiVersion: v1
2+
kind: ServiceAccount
3+
metadata:
4+
name: test-extension
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright 2022 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 lifecycle contains the handlers for the lifecycle hooks.
18+
package lifecycle
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"github.com/pkg/errors"
24+
corev1 "k8s.io/api/core/v1"
25+
"k8s.io/apimachinery/pkg/types"
26+
ctrl "sigs.k8s.io/controller-runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/yaml"
29+
30+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
31+
runtimecatalog "sigs.k8s.io/cluster-api/internal/runtime/catalog"
32+
)
33+
34+
// Handler is the handler for the lifecycle hooks.
35+
type Handler struct {
36+
Client client.Client
37+
}
38+
39+
// DoBeforeClusterCreate implements the BeforeClusterCreate hook.
40+
func (h *Handler) DoBeforeClusterCreate(ctx context.Context, request *runtimehooksv1.BeforeClusterCreateRequest, response *runtimehooksv1.BeforeClusterCreateResponse) {
41+
log := ctrl.LoggerFrom(ctx)
42+
log.Info("BeforeClusterCreate is called")
43+
cluster := request.Cluster
44+
if err := h.recordCallInConfigMap(cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterCreate); err != nil {
45+
response.Status = runtimehooksv1.ResponseStatusFailure
46+
response.Message = err.Error()
47+
return
48+
}
49+
log.Info("BeforeClusterCreate has been recorded in configmap", "cm", cluster.Name+"-hookresponses")
50+
51+
err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterCreate, response)
52+
if err != nil {
53+
response.Status = runtimehooksv1.ResponseStatusFailure
54+
response.Message = err.Error()
55+
return
56+
}
57+
}
58+
59+
// DoBeforeClusterUpgrade implements the BeforeClusterUpgrade hook.
60+
func (h *Handler) DoBeforeClusterUpgrade(ctx context.Context, request *runtimehooksv1.BeforeClusterUpgradeRequest, response *runtimehooksv1.BeforeClusterUpgradeResponse) {
61+
log := ctrl.LoggerFrom(ctx)
62+
log.Info("BeforeClusterUpgrade is called")
63+
cluster := request.Cluster
64+
if err := h.recordCallInConfigMap(cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterUpgrade); err != nil {
65+
response.Status = runtimehooksv1.ResponseStatusFailure
66+
response.Message = err.Error()
67+
return
68+
}
69+
err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterUpgrade, response)
70+
if err != nil {
71+
response.Status = runtimehooksv1.ResponseStatusFailure
72+
response.Message = err.Error()
73+
return
74+
}
75+
}
76+
77+
// DoAfterControlPlaneInitialized implements the AfterControlPlaneInitialized hook.
78+
func (h *Handler) DoAfterControlPlaneInitialized(ctx context.Context, request *runtimehooksv1.AfterControlPlaneInitializedRequest, response *runtimehooksv1.AfterControlPlaneInitializedResponse) {
79+
log := ctrl.LoggerFrom(ctx)
80+
log.Info("AfterControlPlaneInitialized is called")
81+
cluster := request.Cluster
82+
if err := h.recordCallInConfigMap(cluster.Name, cluster.Namespace, runtimehooksv1.AfterControlPlaneInitialized); err != nil {
83+
response.Status = runtimehooksv1.ResponseStatusFailure
84+
response.Message = err.Error()
85+
return
86+
}
87+
err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.AfterControlPlaneInitialized, response)
88+
if err != nil {
89+
response.Status = runtimehooksv1.ResponseStatusFailure
90+
response.Message = err.Error()
91+
return
92+
}
93+
}
94+
95+
// DoAfterControlPlaneUpgrade implements the AfterControlPlaneUpgrade hook.
96+
func (h *Handler) DoAfterControlPlaneUpgrade(ctx context.Context, request *runtimehooksv1.AfterControlPlaneUpgradeRequest, response *runtimehooksv1.AfterControlPlaneUpgradeResponse) {
97+
log := ctrl.LoggerFrom(ctx)
98+
log.Info("AfterControlPlaneUpgrade is called")
99+
cluster := request.Cluster
100+
if err := h.recordCallInConfigMap(cluster.Name, cluster.Namespace, runtimehooksv1.AfterControlPlaneUpgrade); err != nil {
101+
response.Status = runtimehooksv1.ResponseStatusFailure
102+
response.Message = err.Error()
103+
return
104+
}
105+
err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.AfterControlPlaneUpgrade, response)
106+
if err != nil {
107+
response.Status = runtimehooksv1.ResponseStatusFailure
108+
response.Message = err.Error()
109+
return
110+
}
111+
}
112+
113+
// DoAfterClusterUpgrade implements the AfterClusterUpgrade hook.
114+
func (h *Handler) DoAfterClusterUpgrade(ctx context.Context, request *runtimehooksv1.AfterClusterUpgradeRequest, response *runtimehooksv1.AfterClusterUpgradeResponse) {
115+
log := ctrl.LoggerFrom(ctx)
116+
log.Info("AfterClusterUpgrade is called")
117+
cluster := request.Cluster
118+
if err := h.recordCallInConfigMap(cluster.Name, cluster.Namespace, runtimehooksv1.AfterClusterUpgrade); err != nil {
119+
response.Status = runtimehooksv1.ResponseStatusFailure
120+
response.Message = err.Error()
121+
return
122+
}
123+
err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.AfterClusterUpgrade, response)
124+
if err != nil {
125+
response.Status = runtimehooksv1.ResponseStatusFailure
126+
response.Message = err.Error()
127+
return
128+
}
129+
}
130+
131+
func (h *Handler) readResponseFromConfigMap(ctx context.Context, name, namespace string, hook runtimecatalog.Hook, response runtimehooksv1.ResponseObject) error {
132+
hookName := runtimecatalog.HookName(hook)
133+
configMap := &corev1.ConfigMap{}
134+
configMapName := name + "-hookresponses"
135+
if err := h.Client.Get(context.Background(), client.ObjectKey{Namespace: namespace, Name: configMapName}, configMap); err != nil {
136+
return errors.Wrapf(err, "failed to read the ConfigMap %s/%s", namespace, configMapName)
137+
}
138+
if err := yaml.Unmarshal([]byte(configMap.Data[hookName+"-response"]), response); err != nil {
139+
return errors.Wrapf(err, "failed to read %q response information from ConfigMap", hook)
140+
}
141+
return nil
142+
}
143+
144+
func (h *Handler) recordCallInConfigMap(name, namespace string, hook runtimecatalog.Hook) error {
145+
hookName := runtimecatalog.HookName(hook)
146+
configMap := &corev1.ConfigMap{}
147+
configMapName := name + "-hookresponses"
148+
if err := h.Client.Get(context.Background(), client.ObjectKey{Namespace: namespace, Name: configMapName}, configMap); err != nil {
149+
return errors.Wrapf(err, "failed to read the ConfigMap %s/%s", namespace, configMapName)
150+
}
151+
152+
patch := client.RawPatch(types.MergePatchType,
153+
[]byte(fmt.Sprintf("{\"data\":{\"%s-called\":\"true\"}}", hookName)))
154+
if err := h.Client.Patch(context.Background(), configMap, patch); err != nil {
155+
return errors.Wrapf(err, "failed to update the ConfigMap %s/%s", namespace, configMapName)
156+
}
157+
return nil
158+
}

0 commit comments

Comments
 (0)