diff --git a/pkg/model/resource/webhooks.go b/pkg/model/resource/webhooks.go index 81bab17764c..c51ee64fb92 100644 --- a/pkg/model/resource/webhooks.go +++ b/pkg/model/resource/webhooks.go @@ -35,6 +35,12 @@ type Webhooks struct { Conversion bool `json:"conversion,omitempty"` Spoke []string `json:"spoke,omitempty"` + + // ValidatingCustomPath specifies a custom path for the validating webhook + ValidatingCustomPath string `json:"validatingCustomPath,omitempty"` + + // DefaultingCustomPath specifies a custom path for the defaulting webhook + DefaultingCustomPath string `json:"defaultingCustomPath,omitempty"` } // Validate checks that the Webhooks is valid. diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index 6c8b0c8b914..3a9c95778ac 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -71,6 +71,12 @@ type Options struct { DoValidation bool DoConversion bool + // ValidatingWebhookCustomPath defines the custom path that will be used by the scaffolded validating webhooks + ValidatingWebhookCustomPath string + + // DefaultingWebhookCustomPath defines the custom path that will be used by the scaffolded defaulting webhooks + DefaultingWebhookCustomPath string + // Spoke versions for conversion webhook Spoke []string } @@ -110,6 +116,14 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { } } + if opts.ValidatingWebhookCustomPath != "" { + res.Webhooks.ValidatingCustomPath = opts.ValidatingWebhookCustomPath + } + + if opts.DefaultingWebhookCustomPath != "" { + res.Webhooks.DefaultingCustomPath = opts.DefaultingWebhookCustomPath + } + if len(opts.ExternalAPIPath) > 0 { res.External = true } 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 40b5eee2b2f..66386b45e3a 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go @@ -131,6 +131,12 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { {{- if .Resource.HasDefaultingWebhook }} WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}). {{- end }} + {{- if ne .Resource.Webhooks.ValidatingCustomPath "" }} + WithValidatorCustomPath("{{ .Resource.Webhooks.ValidatingCustomPath }}"). + {{- end }} + {{- if ne .Resource.Webhooks.DefaultingCustomPath "" }} + WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingCustomPath }}"). + {{- end }} Complete() } {{- else }} @@ -148,6 +154,12 @@ func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error { {{- if .Resource.HasDefaultingWebhook }} WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}). {{- end }} + {{- if ne .Resource.Webhooks.ValidatingCustomPath "" }} + WithValidatorCustomPath("{{ .Resource.Webhooks.ValidatingCustomPath }}"). + {{- end }} + {{- if ne .Resource.Webhooks.DefaultingCustomPath "" }} + WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingCustomPath }}"). + {{- end }} Complete() } {{- end }} @@ -157,7 +169,7 @@ func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error { //nolint:lll defaultingWebhookTemplate = ` -// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path={{ if ne .Resource.Webhooks.DefaultingCustomPath "" }}{{ .Resource.Webhooks.DefaultingCustomPath }}{{ else }}/mutate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ end }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} {{ if .IsLegacyPath -}} // +kubebuilder:object:generate=false @@ -197,7 +209,8 @@ func (d *{{ .Resource.Kind }}CustomDefaulter) Default(_ context.Context, obj run // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. -// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path={{ if ne .Resource.Webhooks.ValidatingCustomPath "" }}{{ .Resource.Webhooks.ValidatingCustomPath }}{{ else }}/validate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ end }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} + {{ if .IsLegacyPath -}} // +kubebuilder:object:generate=false diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go index 8e70df13242..011c111cbd0 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -19,6 +19,7 @@ package v4 import ( "errors" "fmt" + "regexp" "strings" "github.com/spf13/pflag" @@ -103,6 +104,12 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") + + fs.StringVar(&p.options.ValidatingWebhookCustomPath, "validating-custom-path", "", + "if set, use the defined custom path for the validating webhook") + + fs.StringVar(&p.options.DefaultingWebhookCustomPath, "defaulting-custom-path", "", + "if set, use the defined custom path for the defaulting webhook") } func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { @@ -125,6 +132,26 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { res.Webhooks.Spoke = append(res.Webhooks.Spoke, spoke) } + const webhookPathStringValidation = `^((/[a-zA-Z0-9-_]+)+|/)$` + // Check if the validating custom webhook path respect the regex + if p.options.ValidatingWebhookCustomPath != "" { + validWebhookPathRegex := regexp.MustCompile(webhookPathStringValidation) + if !validWebhookPathRegex.MatchString(p.options.ValidatingWebhookCustomPath) { + return errors.New( + "validatingCustomPath \"" + p.options.ValidatingWebhookCustomPath + "\" does not match this regex: " + + webhookPathStringValidation) + } + } + // Check if the defaulting custom webhook path respect the regex + if p.options.DefaultingWebhookCustomPath != "" { + validWebhookPathRegex := regexp.MustCompile(webhookPathStringValidation) + if !validWebhookPathRegex.MatchString(p.options.DefaultingWebhookCustomPath) { + return errors.New( + "defaultingCustomPath \"" + p.options.DefaultingWebhookCustomPath + "\" does not match this regex: " + + webhookPathStringValidation) + } + } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { diff --git a/test/e2e/v4/generate_test.go b/test/e2e/v4/generate_test.go index e6a3077010e..33087975aca 100644 --- a/test/e2e/v4/generate_test.go +++ b/test/e2e/v4/generate_test.go @@ -195,6 +195,55 @@ func GenerateV4WithNetworkPolicies(kbc *utils.TestContext) { uncommentKustomizeCoversion(kbc) } +// GenerateV4WithWebhookCustomPath implements a go/v4 plugin project defined by a TestContext. +func GenerateV4WithWebhookCustomPath(kbc *utils.TestContext) { + initingTheProject(kbc) + creatingAPI(kbc) + + By("scaffolding defaulting and validating webhooks") + err := kbc.CreateWebhook( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--defaulting", + "--programmatic-validation", + "--validating-custom-path", "/my-validating-custom-path/my-webhook-handler", + "--defaulting-custom-path", "/my-defaulting-custom-path/my-webhook-handler", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to scaffold webhooks") + + By("implementing the defaulting and validating webhooks") + webhookFilePath := filepath.Join( + kbc.Dir, "internal/webhook", kbc.Version, + fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind))) + err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) + Expect(err).NotTo(HaveOccurred(), "Failed to implement webhooks") + + scaffoldConversionWebhook(kbc) + + ExpectWithOffset(1, pluginutil.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + "#- ../certmanager", "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + "#- ../prometheus", "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode(filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + `#replacements:`, "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode(filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + certManagerTarget, "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode( + filepath.Join(kbc.Dir, "config", "prometheus", "kustomization.yaml"), + monitorTLSPatch, "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + metricsCertPatch, "#")).To(Succeed()) + ExpectWithOffset(1, pluginutil.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + metricsCertReplaces, "#")).To(Succeed()) + uncommentKustomizeCoversion(kbc) +} + // GenerateV4WithoutWebhooks implements a go/v4 plugin with APIs and enable Prometheus and CertManager func GenerateV4WithoutWebhooks(kbc *utils.TestContext) { initingTheProject(kbc) diff --git a/test/e2e/v4/plugin_cluster_test.go b/test/e2e/v4/plugin_cluster_test.go index fd0272274f2..6590d57e223 100644 --- a/test/e2e/v4/plugin_cluster_test.go +++ b/test/e2e/v4/plugin_cluster_test.go @@ -66,6 +66,10 @@ var _ = Describe("kubebuilder", func() { By("removing controller image and working dir") kbc.Destroy() }) + It("should generate a runnable project using the custom path for the webhooks", func() { + GenerateV4WithWebhookCustomPath(kbc) + Run(kbc, true, false, false, true, false) + }) It("should generate a runnable project", func() { GenerateV4(kbc) Run(kbc, true, false, false, true, false)