Skip to content

Commit e7a133f

Browse files
committed
feat: add e2e test to validate webhook conversion between versions
1 parent 72d4edb commit e7a133f

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed

docs/book/src/multiversion-tutorial/testdata/project/config/samples/batch_v1_cronjob.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ spec:
1313
spec:
1414
template:
1515
spec:
16+
securityContext:
17+
runAsNonRoot: true
18+
runAsUser: 1000
19+
seccompProfile:
20+
type: RuntimeDefault
1621
containers:
1722
- name: hello
1823
image: busybox
1924
args:
2025
- /bin/sh
2126
- -c
2227
- date; echo Hello from the Kubernetes cluster
28+
securityContext:
29+
allowPrivilegeEscalation: false
30+
capabilities:
31+
drop:
32+
- ALL
33+
readOnlyRootFilesystem: false
2334
restartPolicy: OnFailure
2435

docs/book/src/multiversion-tutorial/testdata/project/config/samples/batch_v2_cronjob.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ spec:
1414
spec:
1515
template:
1616
spec:
17+
securityContext:
18+
runAsNonRoot: true
19+
runAsUser: 1000
20+
seccompProfile:
21+
type: RuntimeDefault
1722
containers:
1823
- name: hello
1924
image: busybox
2025
args:
2126
- /bin/sh
2227
- -c
2328
- date; echo Hello from the Kubernetes cluster
29+
securityContext:
30+
allowPrivilegeEscalation: false
31+
capabilities:
32+
drop:
33+
- ALL
34+
readOnlyRootFilesystem: false
2435
restartPolicy: OnFailure
2536

docs/book/src/multiversion-tutorial/testdata/project/test/e2e/e2e_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"os/exec"
2727
"path/filepath"
2828
"time"
29+
"strings"
2930

3031
. "github.com/onsi/ginkgo/v2"
3132
. "github.com/onsi/gomega"
@@ -97,6 +98,12 @@ var _ = Describe("Manager", Ordered, func() {
9798
// After each test, check for failures and collect logs, events,
9899
// and pod descriptions for debugging.
99100
AfterEach(func() {
101+
By("Cleaning up test CronJob resources")
102+
cmd := exec.Command("kubectl", "delete", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
103+
_, _ = utils.Run(cmd)
104+
cmd = exec.Command("kubectl", "delete", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
105+
_, _ = utils.Run(cmd)
106+
100107
specReport := CurrentSpecReport()
101108
if specReport.Failed() {
102109
By("Fetching controller manager pod logs")
@@ -331,6 +338,79 @@ var _ = Describe("Manager", Ordered, func() {
331338
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
332339
// strings.ToLower(<Kind>),
333340
// ))
341+
It("should successfully convert between v1 and v2 versions", func() {
342+
By("waiting for the webhook service to be ready")
343+
Eventually(func(g Gomega) {
344+
cmd := exec.Command("kubectl", "get", "endpoints", "-n", namespace,
345+
"-l", "control-plane=controller-manager",
346+
"-o", "jsonpath={.items[0].subsets[0].addresses[0].ip}")
347+
output, err := utils.Run(cmd)
348+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get webhook service endpoints")
349+
g.Expect(strings.TrimSpace(output)).NotTo(BeEmpty(), "Webhook endpoint should have an IP")
350+
}, time.Minute, time.Second).Should(Succeed())
351+
352+
By("creating a v1 CronJob with a specific schedule")
353+
cmd := exec.Command("kubectl", "apply", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace)
354+
_, err := utils.Run(cmd)
355+
Expect(err).NotTo(HaveOccurred(), "Failed to create v1 CronJob")
356+
357+
By("waiting for the v1 CronJob to be created")
358+
Eventually(func(g Gomega) {
359+
cmd := exec.Command("kubectl", "get", "cronjob.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace)
360+
output, err := utils.Run(cmd)
361+
if err != nil {
362+
// Log controller logs on failure for debugging
363+
logCmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail=50")
364+
logs, _ := utils.Run(logCmd)
365+
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs when CronJob not found:\n%s\n", logs)
366+
}
367+
g.Expect(err).NotTo(HaveOccurred(), "v1 CronJob should exist, output: "+output)
368+
}, time.Minute, time.Second).Should(Succeed())
369+
370+
By("fetching the v1 CronJob and verifying the schedule format")
371+
cmd = exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
372+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
373+
v1Schedule, err := utils.Run(cmd)
374+
Expect(err).NotTo(HaveOccurred(), "Failed to get v1 CronJob schedule")
375+
Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
376+
"v1 schedule should be in cron format")
377+
378+
By("fetching the same CronJob as v2 and verifying the converted schedule")
379+
Eventually(func(g Gomega) {
380+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
381+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
382+
v2Minute, err := utils.Run(cmd)
383+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
384+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
385+
"v2 schedule.minute should be converted from v1 schedule")
386+
}, time.Minute, time.Second).Should(Succeed())
387+
388+
By("creating a v2 CronJob with structured schedule fields")
389+
cmd = exec.Command("kubectl", "apply", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace)
390+
_, err = utils.Run(cmd)
391+
Expect(err).NotTo(HaveOccurred(), "Failed to create v2 CronJob")
392+
393+
By("verifying the v2 CronJob has the correct structured schedule")
394+
Eventually(func(g Gomega) {
395+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
396+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
397+
v2Minute, err := utils.Run(cmd)
398+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
399+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
400+
"v2 CronJob should have minute field set")
401+
}, time.Minute, time.Second).Should(Succeed())
402+
403+
By("fetching the v2 CronJob as v1 and verifying schedule conversion")
404+
Eventually(func(g Gomega) {
405+
cmd := exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
406+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
407+
v1Schedule, err := utils.Run(cmd)
408+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get converted v1 schedule")
409+
// When v2 only has minute field set, it converts to "*/1 * * * *"
410+
g.Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
411+
"v1 schedule should be converted from v2 structured schedule")
412+
}, time.Minute, time.Second).Should(Succeed())
413+
})
334414
})
335415
})
336416

hack/docs/internal/multiversion-tutorial/generate_multiversion.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ func (sp *Sample) UpdateTutorial() {
110110

111111
sp.updateConversionFiles()
112112
sp.updateSampleV2()
113+
sp.updateSampleSecurityContext()
113114
sp.updateMain()
115+
sp.updateE2EWebhookConversion()
114116
}
115117

116118
func (sp *Sample) updateCronjobV1DueForce() {
@@ -300,6 +302,84 @@ func (sp *Sample) updateSampleV2() {
300302
hackutils.CheckError("replacing TODO with sampleV2Code in batch_v2_cronjob.yaml", err)
301303
}
302304

305+
func (sp *Sample) updateSampleSecurityContext() {
306+
// Update batch_v1_cronjob.yaml with security context
307+
pathV1 := filepath.Join(sp.ctx.Dir, "config/samples/batch_v1_cronjob.yaml")
308+
oldTextV1 := ` template:
309+
spec:
310+
containers:
311+
- name: hello
312+
image: busybox
313+
args:
314+
- /bin/sh
315+
- -c
316+
- date; echo Hello from the Kubernetes cluster
317+
restartPolicy: OnFailure`
318+
319+
newTextV1 := ` template:
320+
spec:
321+
securityContext:
322+
runAsNonRoot: true
323+
runAsUser: 1000
324+
seccompProfile:
325+
type: RuntimeDefault
326+
containers:
327+
- name: hello
328+
image: busybox
329+
args:
330+
- /bin/sh
331+
- -c
332+
- date; echo Hello from the Kubernetes cluster
333+
securityContext:
334+
allowPrivilegeEscalation: false
335+
capabilities:
336+
drop:
337+
- ALL
338+
readOnlyRootFilesystem: false
339+
restartPolicy: OnFailure`
340+
341+
err := pluginutil.ReplaceInFile(pathV1, oldTextV1, newTextV1)
342+
hackutils.CheckError("updating security context in batch_v1_cronjob.yaml", err)
343+
344+
// Update batch_v2_cronjob.yaml with security context
345+
pathV2 := filepath.Join(sp.ctx.Dir, "config/samples/batch_v2_cronjob.yaml")
346+
oldTextV2 := ` template:
347+
spec:
348+
containers:
349+
- name: hello
350+
image: busybox
351+
args:
352+
- /bin/sh
353+
- -c
354+
- date; echo Hello from the Kubernetes cluster
355+
restartPolicy: OnFailure`
356+
357+
newTextV2 := ` template:
358+
spec:
359+
securityContext:
360+
runAsNonRoot: true
361+
runAsUser: 1000
362+
seccompProfile:
363+
type: RuntimeDefault
364+
containers:
365+
- name: hello
366+
image: busybox
367+
args:
368+
- /bin/sh
369+
- -c
370+
- date; echo Hello from the Kubernetes cluster
371+
securityContext:
372+
allowPrivilegeEscalation: false
373+
capabilities:
374+
drop:
375+
- ALL
376+
readOnlyRootFilesystem: false
377+
restartPolicy: OnFailure`
378+
379+
err = pluginutil.ReplaceInFile(pathV2, oldTextV2, newTextV2)
380+
hackutils.CheckError("updating security context in batch_v2_cronjob.yaml", err)
381+
}
382+
303383
func (sp *Sample) updateConversionFiles() {
304384
path := filepath.Join(sp.ctx.Dir, "api/v1/cronjob_conversion.go")
305385

@@ -790,3 +870,119 @@ func (sp *Sample) CodeGen() {
790870
err = sp.ctx.EditHelmPlugin()
791871
hackutils.CheckError("Failed to enable helm plugin", err)
792872
}
873+
874+
const webhookConversionE2ETest = `
875+
It("should successfully convert between v1 and v2 versions", func() {
876+
By("waiting for the webhook service to be ready")
877+
Eventually(func(g Gomega) {
878+
cmd := exec.Command("kubectl", "get", "endpoints", "-n", namespace,
879+
"-l", "control-plane=controller-manager",
880+
"-o", "jsonpath={.items[0].subsets[0].addresses[0].ip}")
881+
output, err := utils.Run(cmd)
882+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get webhook service endpoints")
883+
g.Expect(strings.TrimSpace(output)).NotTo(BeEmpty(), "Webhook endpoint should have an IP")
884+
}, time.Minute, time.Second).Should(Succeed())
885+
886+
By("creating a v1 CronJob with a specific schedule")
887+
cmd := exec.Command("kubectl", "apply", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace)
888+
_, err := utils.Run(cmd)
889+
Expect(err).NotTo(HaveOccurred(), "Failed to create v1 CronJob")
890+
891+
By("waiting for the v1 CronJob to be created")
892+
Eventually(func(g Gomega) {
893+
cmd := exec.Command("kubectl", "get", "cronjob.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace)
894+
output, err := utils.Run(cmd)
895+
if err != nil {
896+
// Log controller logs on failure for debugging
897+
logCmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail=50")
898+
logs, _ := utils.Run(logCmd)
899+
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs when CronJob not found:\n%s\n", logs)
900+
}
901+
g.Expect(err).NotTo(HaveOccurred(), "v1 CronJob should exist, output: "+output)
902+
}, time.Minute, time.Second).Should(Succeed())
903+
904+
By("fetching the v1 CronJob and verifying the schedule format")
905+
cmd = exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
906+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
907+
v1Schedule, err := utils.Run(cmd)
908+
Expect(err).NotTo(HaveOccurred(), "Failed to get v1 CronJob schedule")
909+
Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
910+
"v1 schedule should be in cron format")
911+
912+
By("fetching the same CronJob as v2 and verifying the converted schedule")
913+
Eventually(func(g Gomega) {
914+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
915+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
916+
v2Minute, err := utils.Run(cmd)
917+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
918+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
919+
"v2 schedule.minute should be converted from v1 schedule")
920+
}, time.Minute, time.Second).Should(Succeed())
921+
922+
By("creating a v2 CronJob with structured schedule fields")
923+
cmd = exec.Command("kubectl", "apply", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace)
924+
_, err = utils.Run(cmd)
925+
Expect(err).NotTo(HaveOccurred(), "Failed to create v2 CronJob")
926+
927+
By("verifying the v2 CronJob has the correct structured schedule")
928+
Eventually(func(g Gomega) {
929+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
930+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
931+
v2Minute, err := utils.Run(cmd)
932+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
933+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
934+
"v2 CronJob should have minute field set")
935+
}, time.Minute, time.Second).Should(Succeed())
936+
937+
By("fetching the v2 CronJob as v1 and verifying schedule conversion")
938+
Eventually(func(g Gomega) {
939+
cmd := exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
940+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
941+
v1Schedule, err := utils.Run(cmd)
942+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get converted v1 schedule")
943+
// When v2 only has minute field set, it converts to "*/1 * * * *"
944+
g.Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
945+
"v1 schedule should be converted from v2 structured schedule")
946+
}, time.Minute, time.Second).Should(Succeed())
947+
})`
948+
949+
func (sp *Sample) updateE2EWebhookConversion() {
950+
cronjobE2ETest := filepath.Join(sp.ctx.Dir, "test", "e2e", "e2e_test.go")
951+
952+
// Add strings import if not already present
953+
err := pluginutil.InsertCodeIfNotExist(cronjobE2ETest,
954+
` "os/exec"
955+
"path/filepath"
956+
"time"`,
957+
`
958+
"strings"`)
959+
hackutils.CheckError("adding strings import for e2e test", err)
960+
961+
// Add CronJob cleanup to the AfterEach block
962+
err = pluginutil.InsertCode(cronjobE2ETest,
963+
` // After each test, check for failures and collect logs, events,
964+
// and pod descriptions for debugging.
965+
AfterEach(func() {`,
966+
`
967+
By("Cleaning up test CronJob resources")
968+
cmd := exec.Command("kubectl", "delete", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
969+
_, _ = utils.Run(cmd)
970+
cmd = exec.Command("kubectl", "delete", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
971+
_, _ = utils.Run(cmd)
972+
`)
973+
hackutils.CheckError("adding CronJob cleanup to AfterEach", err)
974+
975+
// Add webhook conversion test after the existing TODO comment
976+
err = pluginutil.InsertCode(cronjobE2ETest,
977+
` // TODO: Customize the e2e test suite with scenarios specific to your project.
978+
// Consider applying sample/CR(s) and check their status and/or verifying
979+
// the reconciliation by using the metrics, i.e.:
980+
// metricsOutput, err := getMetricsOutput()
981+
// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
982+
// Expect(metricsOutput).To(ContainSubstring(
983+
// fmt.Sprintf(`+"`"+`controller_runtime_reconcile_total{controller="%s",result="success"} 1`+"`"+`,
984+
// strings.ToLower(<Kind>),
985+
// ))`,
986+
webhookConversionE2ETest)
987+
hackutils.CheckError("adding webhook conversion e2e test", err)
988+
}

0 commit comments

Comments
 (0)