Skip to content

Commit

Permalink
Fix issue with scaffolding multiple webhooks for the same resource
Browse files Browse the repository at this point in the history
Previously, attempting to create multiple webhooks for the same resource with different configurations (e.g., `--defaulting` and `--programmatic-validation`) resulted in an error indicating that the webhook already exists.

This update allows multiple webhook types to be scaffolded for a single resource without conflict, providing flexibility for users be able to scaffold different types of webhooks for the same GKV afterwords and without the need to be all by once.
  • Loading branch information
camilamacedo86 committed Nov 4, 2024
1 parent 5f8342e commit a7fab10
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 6 additions & 3 deletions pkg/plugins/golang/v4/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions test/e2e/v4/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v4

import (
"fmt"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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")
}
69 changes: 69 additions & 0 deletions test/e2e/v4/plugin_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}
}

Expand Down

0 comments on commit a7fab10

Please sign in to comment.