Skip to content

Commit 5ce0387

Browse files
authored
Merge pull request #4286 from camilamacedo86/e2e-test-go-v4-conversion
🐛 (go/v4) Fix issue with scaffolding multiple webhooks for the same resource
2 parents 5f8342e + ae3bcf6 commit 5ce0387

File tree

4 files changed

+194
-8
lines changed

4 files changed

+194
-8
lines changed

pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (f *Webhook) SetTemplateDefaults() error {
8181
if f.Force {
8282
f.IfExistsAction = machinery.OverwriteFile
8383
} else {
84-
f.IfExistsAction = machinery.Error
84+
f.IfExistsAction = machinery.SkipFile
8585
}
8686

8787
f.AdmissionReviewVersions = "v1"

pkg/plugins/golang/v4/webhook.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,12 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
119119
return err
120120
}
121121

122-
if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
123-
return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
124-
" --programmatic-validation and --conversion to be true", p.commandName)
122+
// Ensure at least one webhook type is specified
123+
if !p.resource.HasDefaultingWebhook() &&
124+
!p.resource.HasValidationWebhook() &&
125+
!p.resource.HasConversionWebhook() {
126+
return fmt.Errorf("%s create webhook requires at least one of --defaulting, --programmatic-validation, "+
127+
"and --conversion to be true", p.commandName)
125128
}
126129

127130
// check if resource exist to create webhook

test/e2e/v4/generate_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v4
1818

1919
import (
2020
"fmt"
21+
"os"
2122
"path/filepath"
2223
"strings"
2324

@@ -55,6 +56,8 @@ func GenerateV4(kbc *utils.TestContext) {
5556
err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
5657
ExpectWithOffset(1, err).NotTo(HaveOccurred())
5758

59+
scaffoldConversionWebhook(kbc)
60+
5861
ExpectWithOffset(1, pluginutil.UncommentCode(
5962
filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
6063
"#- ../certmanager", "#")).To(Succeed())
@@ -92,6 +95,8 @@ func GenerateV4WithoutMetrics(kbc *utils.TestContext) {
9295
err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
9396
ExpectWithOffset(1, err).NotTo(HaveOccurred())
9497

98+
scaffoldConversionWebhook(kbc)
99+
95100
ExpectWithOffset(1, pluginutil.UncommentCode(
96101
filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
97102
"#- ../certmanager", "#")).To(Succeed())
@@ -153,6 +158,8 @@ func GenerateV4WithNetworkPolicies(kbc *utils.TestContext) {
153158
err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
154159
ExpectWithOffset(1, err).NotTo(HaveOccurred())
155160

161+
scaffoldConversionWebhook(kbc)
162+
156163
ExpectWithOffset(1, pluginutil.UncommentCode(
157164
filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
158165
"#- ../certmanager", "#")).To(Succeed())
@@ -368,3 +375,114 @@ func uncommentPodStandards(kbc *utils.TestContext) {
368375
ExpectWithOffset(1, err).NotTo(HaveOccurred())
369376
}
370377
}
378+
379+
// scaffoldConversionWebhook sets up conversion webhooks for testing the ConversionTest API
380+
func scaffoldConversionWebhook(kbc *utils.TestContext) {
381+
By("scaffolding conversion webhooks for testing ConversionTest v1 to v2 conversion")
382+
383+
// Create API for v1 (hub) with conversion enabled
384+
err := kbc.CreateAPI(
385+
"--group", kbc.Group,
386+
"--version", "v1",
387+
"--kind", "ConversionTest",
388+
"--controller=true",
389+
"--resource=true",
390+
"--make=false",
391+
)
392+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v1 API for conversion testing")
393+
394+
// Create API for v2 (spoke) without a controller
395+
err = kbc.CreateAPI(
396+
"--group", kbc.Group,
397+
"--version", "v2",
398+
"--kind", "ConversionTest",
399+
"--controller=false",
400+
"--resource=true",
401+
"--make=false",
402+
)
403+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v2 API for conversion testing")
404+
405+
// Create the conversion webhook for v1
406+
By("setting up the conversion webhook for v1")
407+
err = kbc.CreateWebhook(
408+
"--group", kbc.Group,
409+
"--version", "v1",
410+
"--kind", "ConversionTest",
411+
"--conversion",
412+
"--make=false",
413+
)
414+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion webhook for v1")
415+
416+
// Insert Size field in v1
417+
By("implementing the size spec in v1")
418+
ExpectWithOffset(1, pluginutil.InsertCode(
419+
filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"),
420+
"Foo string `json:\"foo,omitempty\"`",
421+
"\n\tSize int `json:\"size,omitempty\"` // Number of desired instances",
422+
)).NotTo(HaveOccurred(), "failed to add size spec to conversiontest_types v1")
423+
424+
// Insert Replicas field in v2
425+
By("implementing the replicas spec in v2")
426+
ExpectWithOffset(1, pluginutil.InsertCode(
427+
filepath.Join(kbc.Dir, "api", "v2", "conversiontest_types.go"),
428+
"Foo string `json:\"foo,omitempty\"`",
429+
"\n\tReplicas int `json:\"replicas,omitempty\"` // Number of replicas",
430+
)).NotTo(HaveOccurred(), "failed to add replicas spec to conversiontest_types v2")
431+
432+
// TODO: Remove the code bellow when we have hub and spoke scaffolded by
433+
// Kubebuilder. Intead of create the file we will replace the TODO(user)
434+
// with the code implementation.
435+
By("implementing markers")
436+
ExpectWithOffset(1, pluginutil.InsertCode(
437+
filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"),
438+
"// +kubebuilder:object:root=true\n// +kubebuilder:subresource:status",
439+
"\n// +kubebuilder:storageversion\n// +kubebuilder:conversion:hub\n",
440+
)).NotTo(HaveOccurred(), "failed to add markers to conversiontest_types v1")
441+
442+
// Create the hub conversion file in v1
443+
By("creating the conversion implementation in v1 as hub")
444+
err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v1", "conversiontest_conversion.go"), []byte(`
445+
package v1
446+
447+
// ConversionTest defines the hub conversion logic.
448+
// Implement the Hub interface to signal that v1 is the hub version.
449+
func (*ConversionTest) Hub() {}
450+
`), 0644)
451+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create hub conversion file in v1")
452+
453+
// Create the conversion file in v2
454+
By("creating the conversion implementation in v2")
455+
err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v2", "conversiontest_conversion.go"), []byte(`
456+
package v2
457+
458+
import (
459+
"log"
460+
461+
"sigs.k8s.io/controller-runtime/pkg/conversion"
462+
v1 "sigs.k8s.io/kubebuilder/v4/api/v1"
463+
)
464+
465+
// ConvertTo converts this ConversionTest to the Hub version (v1).
466+
func (src *ConversionTest) ConvertTo(dstRaw conversion.Hub) error {
467+
dst := dstRaw.(*v1.ConversionTest)
468+
log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion)
469+
470+
// Implement conversion logic from v2 to v1
471+
dst.Spec.Size = src.Spec.Replicas // Convert replicas in v2 to size in v1
472+
473+
return nil
474+
}
475+
476+
// ConvertFrom converts the Hub version (v1) to this ConversionTest (v2).
477+
func (dst *ConversionTest) ConvertFrom(srcRaw conversion.Hub) error {
478+
src := srcRaw.(*v1.ConversionTest)
479+
log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion)
480+
481+
// Implement conversion logic from v1 to v2
482+
dst.Spec.Replicas = src.Spec.Size // Convert size in v1 to replicas in v2
483+
484+
return nil
485+
}
486+
`), 0644)
487+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion file in v2")
488+
}

test/e2e/v4/plugin_cluster_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,27 @@ func Run(kbc *utils.TestContext, hasWebhook, isToUseInstaller, hasMetrics bool,
262262
return nil
263263
}
264264
EventuallyWithOffset(1, verifyCAInjection, time.Minute, time.Second).Should(Succeed())
265+
266+
By("validating that the CA injection is applied for CRD conversion")
267+
crdKind := "ConversionTest"
268+
verifyCAInjection = func() error {
269+
crdOutput, err := kbc.Kubectl.Get(
270+
false,
271+
"customresourcedefinition.apiextensions.k8s.io",
272+
"-o", fmt.Sprintf(
273+
"jsonpath={.items[?(@.spec.names.kind=='%s')].spec.conversion.webhook.clientConfig.caBundle}",
274+
crdKind),
275+
)
276+
ExpectWithOffset(1, err).NotTo(HaveOccurred(),
277+
"failed to get CRD conversion webhook configuration")
278+
279+
// Check if the CA bundle is populated (length > 10 to avoid placeholder values)
280+
ExpectWithOffset(1, len(crdOutput)).To(BeNumerically(">", 10),
281+
"CA bundle should be injected into the CRD")
282+
return nil
283+
}
284+
EventuallyWithOffset(1, verifyCAInjection, time.Minute, time.Second).Should(Succeed(),
285+
"CA injection validation failed")
265286
}
266287

267288
By("creating an instance of the CR")
@@ -341,6 +362,41 @@ func Run(kbc *utils.TestContext, hasWebhook, isToUseInstaller, hasMetrics bool,
341362
By("removing the namespace")
342363
_, err = kbc.Kubectl.Command("delete", "namespace", namespace)
343364
Expect(err).NotTo(HaveOccurred(), "namespace should be removed successfully")
365+
366+
By("validating the conversion")
367+
368+
// Update the ConversionTest CR sample in v1 to set a specific `size`
369+
By("modifying the ConversionTest CR sample to set `size` for conversion testing")
370+
conversionCRFile := filepath.Join("config", "samples",
371+
fmt.Sprintf("%s_v1_conversiontest.yaml", kbc.Group))
372+
conversionCRPath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), conversionCRFile))
373+
Expect(err).To(Not(HaveOccurred()))
374+
375+
// Edit the file to include `size` in the spec field for v1
376+
f, err := os.OpenFile(conversionCRPath, os.O_APPEND|os.O_WRONLY, 0o644)
377+
Expect(err).To(Not(HaveOccurred()))
378+
defer func() {
379+
err = f.Close()
380+
Expect(err).To(Not(HaveOccurred()))
381+
}()
382+
_, err = f.WriteString("\nspec:\n size: 3")
383+
Expect(err).To(Not(HaveOccurred()))
384+
385+
// Apply the ConversionTest Custom Resource in v1
386+
By("applying the modified ConversionTest CR in v1 for conversion")
387+
_, err = kbc.Kubectl.Apply(true, "-f", conversionCRPath)
388+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to apply modified ConversionTest CR")
389+
390+
// TODO: Add validation to check the conversion
391+
// the v2 should have spec.replicas == 3
392+
393+
if hasMetrics {
394+
By("validating conversion metrics to confirm conversion operations")
395+
metricsOutput := getMetricsOutput(kbc)
396+
conversionMetric := `controller_runtime_reconcile_total{controller="conversiontest",result="success"} 1`
397+
ExpectWithOffset(1, metricsOutput).To(ContainSubstring(conversionMetric),
398+
"Expected metric for successful ConversionTest reconciliation")
399+
}
344400
}
345401
}
346402

@@ -384,10 +440,19 @@ func getControllerName(kbc *utils.TestContext) string {
384440
// getMetricsOutput return the metrics output from curl pod
385441
func getMetricsOutput(kbc *utils.TestContext) string {
386442
_, err := kbc.Kubectl.Command(
387-
"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
388-
fmt.Sprintf("--clusterrole=e2e-%s-metrics-reader", kbc.TestSuffix),
389-
fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount))
390-
ExpectWithOffset(1, err).NotTo(HaveOccurred())
443+
"get", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
444+
)
445+
if err != nil && strings.Contains(err.Error(), "NotFound") {
446+
// Create the clusterrolebinding only if it doesn't exist
447+
_, err = kbc.Kubectl.Command(
448+
"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
449+
fmt.Sprintf("--clusterrole=e2e-%s-metrics-reader", kbc.TestSuffix),
450+
fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount),
451+
)
452+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
453+
} else {
454+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to check clusterrolebinding existence")
455+
}
391456

392457
token, err := serviceAccountToken(kbc)
393458
ExpectWithOffset(2, err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)