Skip to content

Commit a305b10

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

File tree

7 files changed

+252
-0
lines changed

7 files changed

+252
-0
lines changed

docs/book/src/cronjob-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_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/cronjob-tutorial/sample.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,24 @@ const cronjobSample = `
2424
spec:
2525
template:
2626
spec:
27+
securityContext:
28+
runAsNonRoot: true
29+
runAsUser: 1000
30+
seccompProfile:
31+
type: RuntimeDefault
2732
containers:
2833
- name: hello
2934
image: busybox
3035
args:
3136
- /bin/sh
3237
- -c
3338
- date; echo Hello from the Kubernetes cluster
39+
securityContext:
40+
allowPrivilegeEscalation: false
41+
capabilities:
42+
drop:
43+
- ALL
44+
readOnlyRootFilesystem: false
3445
restartPolicy: OnFailure`
3546

3647
const certManagerForMetrics = `# - source: # Uncomment the following block to enable certificates for metrics

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

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func (sp *Sample) UpdateTutorial() {
111111
sp.updateConversionFiles()
112112
sp.updateSampleV2()
113113
sp.updateMain()
114+
sp.updateE2EWebhookConversion()
114115
}
115116

116117
func (sp *Sample) updateCronjobV1DueForce() {
@@ -790,3 +791,119 @@ func (sp *Sample) CodeGen() {
790791
err = sp.ctx.EditHelmPlugin()
791792
hackutils.CheckError("Failed to enable helm plugin", err)
792793
}
794+
795+
const webhookConversionE2ETest = `
796+
It("should successfully convert between v1 and v2 versions", func() {
797+
By("waiting for the webhook service to be ready")
798+
Eventually(func(g Gomega) {
799+
cmd := exec.Command("kubectl", "get", "endpoints", "-n", namespace,
800+
"-l", "control-plane=controller-manager",
801+
"-o", "jsonpath={.items[0].subsets[0].addresses[0].ip}")
802+
output, err := utils.Run(cmd)
803+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get webhook service endpoints")
804+
g.Expect(strings.TrimSpace(output)).NotTo(BeEmpty(), "Webhook endpoint should have an IP")
805+
}, time.Minute, time.Second).Should(Succeed())
806+
807+
By("creating a v1 CronJob with a specific schedule")
808+
cmd := exec.Command("kubectl", "apply", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace)
809+
_, err := utils.Run(cmd)
810+
Expect(err).NotTo(HaveOccurred(), "Failed to create v1 CronJob")
811+
812+
By("waiting for the v1 CronJob to be created")
813+
Eventually(func(g Gomega) {
814+
cmd := exec.Command("kubectl", "get", "cronjob.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace)
815+
output, err := utils.Run(cmd)
816+
if err != nil {
817+
// Log controller logs on failure for debugging
818+
logCmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail=50")
819+
logs, _ := utils.Run(logCmd)
820+
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs when CronJob not found:\n%s\n", logs)
821+
}
822+
g.Expect(err).NotTo(HaveOccurred(), "v1 CronJob should exist, output: "+output)
823+
}, time.Minute, time.Second).Should(Succeed())
824+
825+
By("fetching the v1 CronJob and verifying the schedule format")
826+
cmd = exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
827+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
828+
v1Schedule, err := utils.Run(cmd)
829+
Expect(err).NotTo(HaveOccurred(), "Failed to get v1 CronJob schedule")
830+
Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
831+
"v1 schedule should be in cron format")
832+
833+
By("fetching the same CronJob as v2 and verifying the converted schedule")
834+
Eventually(func(g Gomega) {
835+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
836+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
837+
v2Minute, err := utils.Run(cmd)
838+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
839+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
840+
"v2 schedule.minute should be converted from v1 schedule")
841+
}, time.Minute, time.Second).Should(Succeed())
842+
843+
By("creating a v2 CronJob with structured schedule fields")
844+
cmd = exec.Command("kubectl", "apply", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace)
845+
_, err = utils.Run(cmd)
846+
Expect(err).NotTo(HaveOccurred(), "Failed to create v2 CronJob")
847+
848+
By("verifying the v2 CronJob has the correct structured schedule")
849+
Eventually(func(g Gomega) {
850+
cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample",
851+
"-n", namespace, "-o", "jsonpath={.spec.schedule.minute}")
852+
v2Minute, err := utils.Run(cmd)
853+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule")
854+
g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"),
855+
"v2 CronJob should have minute field set")
856+
}, time.Minute, time.Second).Should(Succeed())
857+
858+
By("fetching the v2 CronJob as v1 and verifying schedule conversion")
859+
Eventually(func(g Gomega) {
860+
cmd := exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample",
861+
"-n", namespace, "-o", "jsonpath={.spec.schedule}")
862+
v1Schedule, err := utils.Run(cmd)
863+
g.Expect(err).NotTo(HaveOccurred(), "Failed to get converted v1 schedule")
864+
// When v2 only has minute field set, it converts to "*/1 * * * *"
865+
g.Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"),
866+
"v1 schedule should be converted from v2 structured schedule")
867+
}, time.Minute, time.Second).Should(Succeed())
868+
})`
869+
870+
func (sp *Sample) updateE2EWebhookConversion() {
871+
cronjobE2ETest := filepath.Join(sp.ctx.Dir, "test", "e2e", "e2e_test.go")
872+
873+
// Add strings import if not already present
874+
err := pluginutil.InsertCodeIfNotExist(cronjobE2ETest,
875+
` "os/exec"
876+
"path/filepath"
877+
"time"`,
878+
`
879+
"strings"`)
880+
hackutils.CheckError("adding strings import for e2e test", err)
881+
882+
// Add CronJob cleanup to the AfterEach block
883+
err = pluginutil.InsertCode(cronjobE2ETest,
884+
` // After each test, check for failures and collect logs, events,
885+
// and pod descriptions for debugging.
886+
AfterEach(func() {`,
887+
`
888+
By("Cleaning up test CronJob resources")
889+
cmd := exec.Command("kubectl", "delete", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
890+
_, _ = utils.Run(cmd)
891+
cmd = exec.Command("kubectl", "delete", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace, "--ignore-not-found=true")
892+
_, _ = utils.Run(cmd)
893+
`)
894+
hackutils.CheckError("adding CronJob cleanup to AfterEach", err)
895+
896+
// Add webhook conversion test after the existing TODO comment
897+
err = pluginutil.InsertCode(cronjobE2ETest,
898+
` // TODO: Customize the e2e test suite with scenarios specific to your project.
899+
// Consider applying sample/CR(s) and check their status and/or verifying
900+
// the reconciliation by using the metrics, i.e.:
901+
// metricsOutput, err := getMetricsOutput()
902+
// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
903+
// Expect(metricsOutput).To(ContainSubstring(
904+
// fmt.Sprintf(`+"`"+`controller_runtime_reconcile_total{controller="%s",result="success"} 1`+"`"+`,
905+
// strings.ToLower(<Kind>),
906+
// ))`,
907+
webhookConversionE2ETest)
908+
hackutils.CheckError("adding webhook conversion e2e test", err)
909+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,23 @@ const sampleV2Code = `schedule:
2424
spec:
2525
template:
2626
spec:
27+
securityContext:
28+
runAsNonRoot: true
29+
runAsUser: 1000
30+
seccompProfile:
31+
type: RuntimeDefault
2732
containers:
2833
- name: hello
2934
image: busybox
3035
args:
3136
- /bin/sh
3237
- -c
3338
- date; echo Hello from the Kubernetes cluster
39+
securityContext:
40+
allowPrivilegeEscalation: false
41+
capabilities:
42+
drop:
43+
- ALL
44+
readOnlyRootFilesystem: false
3445
restartPolicy: OnFailure
3546
`

0 commit comments

Comments
 (0)