Skip to content

Commit ae3bcf6

Browse files
Fix issue with scaffolding multiple webhooks for the same resource
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.
1 parent 5f8342e commit ae3bcf6

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)