Skip to content

Commit 275a9e6

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 275a9e6

File tree

8 files changed

+315
-4
lines changed

8 files changed

+315
-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

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

0 commit comments

Comments
 (0)