diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go index a5ac16564bd..0849eff0824 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go @@ -81,7 +81,7 @@ func (f *Webhook) SetTemplateDefaults() error { if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = machinery.Error + f.IfExistsAction = machinery.SkipFile } f.AdmissionReviewVersions = "v1" diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go index 685b216db9d..6f5659ca041 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -119,9 +119,12 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { return err } - if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() { - return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+ - " --programmatic-validation and --conversion to be true", p.commandName) + // Ensure at least one webhook type is specified + if !p.resource.HasDefaultingWebhook() && + !p.resource.HasValidationWebhook() && + !p.resource.HasConversionWebhook() { + return fmt.Errorf("%s create webhook requires at least one of --defaulting, --programmatic-validation, "+ + "and --conversion to be true", p.commandName) } // check if resource exist to create webhook diff --git a/test/e2e/v4/generate_test.go b/test/e2e/v4/generate_test.go index 0a22b102eb8..53e03f2cdaf 100644 --- a/test/e2e/v4/generate_test.go +++ b/test/e2e/v4/generate_test.go @@ -18,6 +18,7 @@ package v4 import ( "fmt" + "os" "path/filepath" "strings" @@ -55,6 +56,8 @@ func GenerateV4(kbc *utils.TestContext) { err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + scaffoldConversionWebhook(kbc) + ExpectWithOffset(1, pluginutil.UncommentCode( filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), "#- ../certmanager", "#")).To(Succeed()) @@ -92,6 +95,8 @@ func GenerateV4WithoutMetrics(kbc *utils.TestContext) { err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + scaffoldConversionWebhook(kbc) + ExpectWithOffset(1, pluginutil.UncommentCode( filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), "#- ../certmanager", "#")).To(Succeed()) @@ -153,6 +158,8 @@ func GenerateV4WithNetworkPolicies(kbc *utils.TestContext) { err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + scaffoldConversionWebhook(kbc) + ExpectWithOffset(1, pluginutil.UncommentCode( filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), "#- ../certmanager", "#")).To(Succeed()) @@ -368,3 +375,118 @@ func uncommentPodStandards(kbc *utils.TestContext) { ExpectWithOffset(1, err).NotTo(HaveOccurred()) } } + +// scaffoldConversionWebhook sets up conversion webhooks for testing the ConversionTest API +func scaffoldConversionWebhook(kbc *utils.TestContext) { + By("scaffolding conversion webhooks for testing ConversionTest v1 to v2 conversion") + + // Create API for v1 (hub) with conversion enabled + err := kbc.CreateAPI( + "--group", kbc.Group, + "--version", "v1", + "--kind", "ConversionTest", + "--controller=true", + "--resource=true", + "--make=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v1 API for conversion testing") + + // Create API for v2 (spoke) without a controller + err = kbc.CreateAPI( + "--group", kbc.Group, + "--version", "v2", + "--kind", "ConversionTest", + "--controller=false", + "--resource=true", + "--make=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v2 API for conversion testing") + + // Create the conversion webhook for v1 + By("setting up the conversion webhook for v1") + err = kbc.CreateWebhook( + "--group", kbc.Group, + "--version", "v1", + "--kind", "ConversionTest", + "--conversion", + "--make=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion webhook for v1") + + // Insert Size field in v1 + By("implementing the size spec in v1") + ExpectWithOffset(1, pluginutil.InsertCode( + filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"), + "Foo string `json:\"foo,omitempty\"`", + "\n\tSize int `json:\"size,omitempty\"` // Number of desired instances", + )).NotTo(HaveOccurred(), "failed to add size spec to conversiontest_types v1") + + // Insert Replicas field in v2 + By("implementing the replicas spec in v2") + ExpectWithOffset(1, pluginutil.InsertCode( + filepath.Join(kbc.Dir, "api", "v2", "conversiontest_types.go"), + "Foo string `json:\"foo,omitempty\"`", + "\n\tReplicas int `json:\"replicas,omitempty\"` // Number of replicas", + )).NotTo(HaveOccurred(), "failed to add replicas spec to conversiontest_types v2") + + // TODO: Remove the code bellow when we have hub and spoke scaffolded by + // Kubebuilder. Intead of create the file we will replace the TODO(user) + // with the code implementation. + By("implementing markers") + ExpectWithOffset(1, pluginutil.InsertCode( + filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"), + "// +kubebuilder:object:root=true\n// +kubebuilder:subresource:status", + "\n// +kubebuilder:storageversion\n// +kubebuilder:conversion:hub\n", + )).NotTo(HaveOccurred(), "failed to add markers to conversiontest_types v1") + + // Create the hub conversion file in v1 + By("creating the conversion implementation in v1 as hub") + err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v1", "conversiontest_conversion.go"), []byte(` +package v1 + +import ( + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConversionTest defines the hub conversion logic. +// Implement the Hub interface to signal that v1 is the hub version. +func (*ConversionTest) Hub() {} +`), 0644) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create hub conversion file in v1") + + // Create the conversion file in v2 + By("creating the conversion implementation in v2") + err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v2", "conversiontest_conversion.go"), []byte(` +package v2 + +import ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + v1 "sigs.k8s.io/kubebuilder/v4/api/v1" +) + +// ConvertTo converts this ConversionTest to the Hub version (v1). +func (src *ConversionTest) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1.ConversionTest) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // Implement conversion logic from v2 to v1 + dst.Spec.Size = src.Spec.Replicas // Convert replicas in v2 to size in v1 + + return nil +} + +// ConvertFrom converts the Hub version (v1) to this ConversionTest (v2). +func (dst *ConversionTest) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1.ConversionTest) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // Implement conversion logic from v1 to v2 + dst.Spec.Replicas = src.Spec.Size // Convert size in v1 to replicas in v2 + + return nil +} +`), 0644) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion file in v2") +} diff --git a/test/e2e/v4/plugin_cluster_test.go b/test/e2e/v4/plugin_cluster_test.go index 6aa21049ff6..af087ebf0ba 100644 --- a/test/e2e/v4/plugin_cluster_test.go +++ b/test/e2e/v4/plugin_cluster_test.go @@ -262,6 +262,26 @@ func Run(kbc *utils.TestContext, hasWebhook, isToUseInstaller, hasMetrics bool, return nil } EventuallyWithOffset(1, verifyCAInjection, time.Minute, time.Second).Should(Succeed()) + + // Validate the CA injection in the CRD conversion webhook + By("validating that the CA injection is applied for CRD conversion") + crdName := fmt.Sprintf("%s.%s", "conversiontests", kbc.Group) + verifyCAInjection = func() error { + crdOutput, err := kbc.Kubectl.Get( + false, + "customresourcedefinition.apiextensions.k8s.io", crdName, + "-o", "jsonpath={.spec.conversion.webhook.clientConfig.caBundle}", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), + "failed to get CRD conversion webhook configuration") + + // Check if the CA bundle is populated (length > 10 to avoid placeholder values) + ExpectWithOffset(1, len(crdOutput)).To(BeNumerically(">", 10), + "CA bundle should be injected into the CRD") + return nil + } + EventuallyWithOffset(1, verifyCAInjection, time.Minute, time.Second).Should(Succeed(), + "CA injection validation failed") } By("creating an instance of the CR") @@ -341,6 +361,55 @@ func Run(kbc *utils.TestContext, hasWebhook, isToUseInstaller, hasMetrics bool, By("removing the namespace") _, err = kbc.Kubectl.Command("delete", "namespace", namespace) Expect(err).NotTo(HaveOccurred(), "namespace should be removed successfully") + + By("validating the conversion") + + // Update the ConversionTest CR sample in v1 to set a specific `size` + By("modifying the ConversionTest CR sample to set `size` for conversion testing") + conversionCRFile := filepath.Join("config", "samples", + fmt.Sprintf("%s_v1_conversiontest.yaml", kbc.Group)) + conversionCRPath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), conversionCRFile)) + Expect(err).To(Not(HaveOccurred())) + + // Edit the file to include `size` in the spec field for v1 + f, err := os.OpenFile(conversionCRPath, os.O_APPEND|os.O_WRONLY, 0o644) + Expect(err).To(Not(HaveOccurred())) + defer func() { + err = f.Close() + Expect(err).To(Not(HaveOccurred())) + }() + _, err = f.WriteString("\nspec:\n size: 3") + Expect(err).To(Not(HaveOccurred())) + + // Apply the ConversionTest Custom Resource in v1 + By("applying the modified ConversionTest CR in v1 for conversion") + _, err = kbc.Kubectl.Apply(true, "-f", conversionCRPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to apply modified ConversionTest CR") + + // Verify that the ConversionTest resource was converted to v2 + By("verifying that the applied CR was converted to v2 in the cluster") + convertedResourceName := "conversiontest-sample" // This should match the name of your sample CR + verifyConversion := func() error { + v2Output, err := kbc.Kubectl.Get( + true, + fmt.Sprintf("conversiontest.v2.%s", kbc.Group), convertedResourceName, + "-o", "jsonpath={.spec.replicas}", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to retrieve v2 version of the ConversionTest resource") + + // Ensure the spec was converted from v1's `size` to v2's `replicas` + expectedReplicas := "3" // This should match the `size` value set in v1 + ExpectWithOffset(1, v2Output).To(Equal(expectedReplicas), "expected replicas value after conversion") + return nil + } + EventuallyWithOffset(1, verifyConversion, time.Minute, time.Second).Should(Succeed(), + "conversion to v2 failed") + + By("validating conversion metrics to confirm conversion operations") + metricsOutput := getMetricsOutput(kbc) + conversionMetric := `controller_runtime_reconcile_total{controller="conversiontest",result="success"} 1` + ExpectWithOffset(1, metricsOutput).To(ContainSubstring(conversionMetric), + "Expected metric for successful ConversionTest reconciliation") } }