Skip to content

✨ Add custom path option for webhooks #4845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/model/resource/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions pkg/plugins/golang/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s great work, thank you! 🎉
The implementation looks solid at a glance.

However, since this introduces API changes, I’ll need a bit more time to review it carefully and fully assess the impact.

A couple of things we should also make sure of before merging:

  • This should be compatible with the alpha generate command — we’ll likely need to include logic to ensure the project is re-scaffolded correctly with the new option.

  • It should also work seamlessly with the Helm charts plugin (helm.kubebuilder.io/v1-alpha) — could you confirm compatibility or add test coverage for that?

Thanks again for the contribution! I’ll follow up with a more detailed review soon.

Copy link
Contributor Author

@damsien damsien Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your quick comment!

we’ll likely need to include logic to ensure the project is re-scaffolded correctly with the new option.

What do you mean by "the project is re-scaffolded"?

could you confirm compatibility or add test coverage for that?

I didn't test it with the helm chart plugin. Also, since I never worked with the helm plugin, I don't really know where to start to confirm the compatibility 😅. Could you please provide me the test file where I should write the new test?

I don't really understand where there should be an incompatibility with the helm plugin since my feature is only related to the webhook file that is created using the CLI (actually it only affects the content of the webhook file, and it does not scaffold any other file). The feature is not related at all with the delivery of the user's operator.


// Spoke versions for conversion webhook
Spoke []string
}
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions pkg/plugins/golang/v4/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v4
import (
"errors"
"fmt"
"regexp"
"strings"

"github.com/spf13/pflag"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions test/e2e/v4/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/v4/plugin_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down