Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ validate anything on deletion.
/*
This marker is responsible for generating a validation webhook manifest.
*/

// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1

// CronJobCustomValidator struct is responsible for validating the CronJob resource
Expand Down
27 changes: 26 additions & 1 deletion docs/book/src/cronjob-tutorial/webhook-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,37 @@ Kubebuilder takes care of the rest for you, such as
1. Creating handlers for your webhooks.
1. Registering each handler with a path in your server.

First, let's scaffold the webhooks for our CRD (CronJob). Well need to run the following command with the `--defaulting` and `--programmatic-validation` flags (since our test project will use defaulting and validating webhooks):
First, let's scaffold the webhooks for our CRD (CronJob). We'll need to run the following command with the `--defaulting` and `--programmatic-validation` flags (since our test project will use defaulting and validating webhooks):

```bash
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
```

This will scaffold the webhook functions and register your webhook with the manager in your `main.go` for you.

## Custom Webhook Paths

You can specify custom HTTP paths for your webhooks using the `--defaulting-path` and `--validation-path` flags:

```bash
# Custom path for defaulting webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --defaulting-path=/my-custom-mutate-path

# Custom path for validation webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob --programmatic-validation --validation-path=/my-custom-validate-path

# Both webhooks with different custom paths
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation \
--defaulting-path=/custom-mutate --validation-path=/custom-validate
```

This changes the path in the webhook marker annotation but does not change where the webhook files are scaffolded. The webhook files will still be created in `internal/webhook/v1/`.

<aside class="note">
<h1>Version Requirements</h1>

Custom webhook paths require **controller-runtime v0.21+**. In earlier versions (< `v0.21`), the webhook path must follow a specific pattern and cannot be customized. The path is automatically generated based on the resource's group, version, and kind (e.g., `/mutate-batch-v1-cronjob`).

</aside>

{{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}}
10 changes: 10 additions & 0 deletions docs/book/src/multiversion-tutorial/conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ The above command will generate the `cronjob_conversion.go` next to our
`cronjob_types.go` file, to avoid
cluttering up our main types file with extra functions.

<aside class="note">
<h1>Conversion Webhooks and Custom Paths</h1>

Unlike defaulting and validation webhooks, conversion webhooks do not support custom paths
via command-line flags. Conversion webhooks use CRD conversion configuration
(`.spec.conversion.webhook.clientConfig.service.path` in the CRD) rather than webhook
marker annotations. The path for conversion webhooks is managed differently and cannot
be customized through kubebuilder flags.
</aside>

## Hub...

First, we'll implement the hub. We'll choose the v1 version as the hub:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ validate anything on deletion.
/*
This marker is responsible for generating a validation webhook manifest.
*/

// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1

// CronJobCustomValidator struct is responsible for validating the CronJob resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ func (d *CronJobCustomDefaulter) Default(_ context.Context, obj runtime.Object)
}

// 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.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=vcronjob-v2.kb.io,admissionReviewVersions=v1

// CronJobCustomValidator struct is responsible for validating the CronJob resource
Expand Down
31 changes: 31 additions & 0 deletions docs/book/src/reference/admission-webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ object after your validation has accepted it.

</aside>

## Custom Webhook Paths

By default, Kubebuilder generates webhook paths based on the resource's group, version, and kind. For example:
- Mutating webhook for `batch/v1/CronJob`: `/mutate-batch-v1-cronjob`
- Validating webhook for `batch/v1/CronJob`: `/validate-batch-v1-cronjob`

You can specify custom paths for webhooks using dedicated flags:

```bash
# Custom path for defaulting webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--defaulting --defaulting-path=/my-custom-mutate-path

# Custom path for validation webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--programmatic-validation --validation-path=/my-custom-validate-path

# Both webhooks with different custom paths
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--defaulting --programmatic-validation \
--defaulting-path=/custom-mutate --validation-path=/custom-validate
```

<aside class="note">
<h1>Version Requirements</h1>

Custom webhook paths require **controller-runtime v0.21+**. In earlier versions (< `v0.21`), the webhook path follows a
fixed pattern based on the resource's group, version, and kind, and cannot be customized.
</aside>


## Handling Resource Status in Admission Webhooks

<aside class="warning">
Expand Down
7 changes: 3 additions & 4 deletions hack/docs/internal/cronjob-tutorial/generate_cronjob.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,12 @@ Then, we set up the webhook with the manager.
err = pluginutil.ReplaceInFile(
filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"),
`// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!`, webhooksNoticeMarker)
hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute", err)
hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute for webhook notice ", err)

err = pluginutil.ReplaceInFile(
filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"),
`// 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.`, explanationValidateCRD)
hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute", err)
`// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.`, explanationValidateCRD)
hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute for explanation validate CRD", err)

err = pluginutil.ReplaceInFile(
filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ validate anything on deletion.

/*
This marker is responsible for generating a validation webhook manifest.
*/`
*/

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.`

const customInterfaceDefaultInfo = `/*
We use the ` + "`" + `webhook.CustomDefaulter` + "`" + `interface to set defaults to our CRD.
Expand Down
7 changes: 7 additions & 0 deletions pkg/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,15 @@ func getWebhookResourceFlags(res resource.Resource) []string {
}
if res.HasValidationWebhook() {
args = append(args, "--programmatic-validation")
if res.Webhooks.ValidationPath != "" {
args = append(args, "--validation-path", res.Webhooks.ValidationPath)
}
}
if res.HasDefaultingWebhook() {
args = append(args, "--defaulting")
if res.Webhooks.DefaultingPath != "" {
args = append(args, "--defaulting-path", res.Webhooks.DefaultingPath)
}
}
if res.HasConversionWebhook() {
args = append(args, "--conversion")
Expand All @@ -473,6 +479,7 @@ func getWebhookResourceFlags(res resource.Resource) []string {
args = append(args, "--spoke", spoke)
}
}
// Note: conversion webhooks don't use custom path flags
}
return args
}
Expand Down
21 changes: 20 additions & 1 deletion pkg/model/resource/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ type Webhooks struct {
Conversion bool `json:"conversion,omitempty"`

Spoke []string `json:"spoke,omitempty"`

// DefaultingPath holds the custom path for the defaulting/mutating webhook.
// This path is used in the +kubebuilder:webhook marker annotation.
DefaultingPath string `json:"defaultingPath,omitempty"`

// ValidationPath holds the custom path for the validation webhook.
// This path is used in the +kubebuilder:webhook marker annotation.
ValidationPath string `json:"validationPath,omitempty"`
}

// Validate checks that the Webhooks is valid.
Expand Down Expand Up @@ -73,6 +81,8 @@ func (webhooks Webhooks) Copy() Webhooks {
Validation: webhooks.Validation,
Conversion: webhooks.Conversion,
Spoke: spokeCopy,
DefaultingPath: webhooks.DefaultingPath,
ValidationPath: webhooks.ValidationPath,
}
}

Expand Down Expand Up @@ -114,14 +124,23 @@ func (webhooks *Webhooks) Update(other *Webhooks) error {
}
}

// Update custom paths (other takes precedence if not empty)
if other.DefaultingPath != "" {
webhooks.DefaultingPath = other.DefaultingPath
}
if other.ValidationPath != "" {
webhooks.ValidationPath = other.ValidationPath
}

return nil
}

// IsEmpty returns if the Webhooks' fields all contain zero-values.
func (webhooks Webhooks) IsEmpty() bool {
return webhooks.WebhookVersion == "" &&
!webhooks.Defaulting && !webhooks.Validation &&
!webhooks.Conversion && len(webhooks.Spoke) == 0
!webhooks.Conversion && len(webhooks.Spoke) == 0 &&
webhooks.DefaultingPath == "" && webhooks.ValidationPath == ""
}

// AddSpoke adds a new spoke version to the Webhooks configuration.
Expand Down
12 changes: 12 additions & 0 deletions pkg/plugins/golang/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ type Options struct {

// Spoke versions for conversion webhook
Spoke []string

// DefaultingPath is the custom path for the defaulting/mutating webhook
DefaultingPath string

// ValidationPath is the custom path for the validation webhook
ValidationPath string
}

// UpdateResource updates the provided resource with the options
Expand Down Expand Up @@ -100,9 +106,15 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) {
res.Webhooks.WebhookVersion = "v1"
if opts.DoDefaulting {
res.Webhooks.Defaulting = true
if opts.DefaultingPath != "" {
res.Webhooks.DefaultingPath = opts.DefaultingPath
}
}
if opts.DoValidation {
res.Webhooks.Validation = true
if opts.ValidationPath != "" {
res.Webhooks.ValidationPath = opts.ValidationPath
}
}
if opts.DoConversion {
res.Webhooks.Conversion = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error {
For(r).
{{- if .Resource.HasValidationWebhook }}
WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
{{- if ne .Resource.Webhooks.ValidationPath "" }}
WithValidatorCustomPath("{{ .Resource.Webhooks.ValidationPath }}").
{{- end }}
{{- end }}
{{- if .Resource.HasDefaultingWebhook }}
WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
{{- if ne .Resource.Webhooks.DefaultingPath "" }}
WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingPath }}").
{{- end }}
{{- end }}
Complete()
}
Expand All @@ -143,9 +149,15 @@ func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error {
{{- end }}
{{- if .Resource.HasValidationWebhook }}
WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
{{- if ne .Resource.Webhooks.ValidationPath "" }}
WithValidatorCustomPath("{{ .Resource.Webhooks.ValidationPath }}").
{{- end }}
{{- end }}
{{- if .Resource.HasDefaultingWebhook }}
WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
{{- if ne .Resource.Webhooks.DefaultingPath "" }}
WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingPath }}").
{{- end }}
{{- end }}
Complete()
}
Expand All @@ -156,7 +168,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 }}{{- if ne .Resource.Webhooks.DefaultingPath "" -}}path={{ .Resource.Webhooks.DefaultingPath }}{{- else -}}path=/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 @@ -194,9 +206,8 @@ func (d *{{ .Resource.Kind }}CustomDefaulter) Default(_ context.Context, obj run
//nolint:lll
validatingWebhookTemplate = `
// 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 }}
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}{{- if ne .Resource.Webhooks.ValidationPath "" -}}path={{ .Resource.Webhooks.ValidationPath }}{{- else -}}path=/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
29 changes: 29 additions & 0 deletions pkg/plugins/golang/v4/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ validating and/or conversion webhooks.
# Create conversion webhook for Group: ship, Version: v1beta1
# and Kind: Frigate
%[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1

# Create defaulting webhook with custom path for Group: ship, Version: v1beta1
# and Kind: Frigate
%[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting \
--defaulting-path=/my-custom-mutate-path

# Create validation webhook with custom path for Group: ship, Version: v1beta1
# and Kind: Frigate
%[1]s create webhook --group ship --version v1beta1 --kind Frigate \
--programmatic-validation --validation-path=/my-custom-validate-path

# Create both defaulting and validation webhooks with different custom paths
%[1]s create webhook --group ship --version v1beta1 --kind Frigate \
--defaulting --programmatic-validation \
--defaulting-path=/custom-mutate --validation-path=/custom-validate
`, cliMeta.CommandName)
}

Expand All @@ -88,6 +103,12 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
nil,
"Comma-separated list of spoke versions to be added to the conversion webhook (e.g., --spoke v1,v2)")

fs.StringVar(&p.options.DefaultingPath, "defaulting-path", "",
"Custom path for the defaulting/mutating webhook (only valid with --defaulting)")

fs.StringVar(&p.options.ValidationPath, "validation-path", "",
"Custom path for the validation webhook (only valid with --programmatic-validation)")

// TODO: remove for go/v5
fs.BoolVar(&p.isLegacyPath, "legacy", false,
"[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+
Expand Down Expand Up @@ -125,6 +146,14 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
res.Webhooks.Spoke = append(res.Webhooks.Spoke, spoke)
}

// Validate path flags are only used with appropriate webhook types
if p.options.DefaultingPath != "" && !p.options.DoDefaulting {
return fmt.Errorf("--defaulting-path can only be used with --defaulting")
}
if p.options.ValidationPath != "" && !p.options.DoValidation {
return fmt.Errorf("--validation-path can only be used with --programmatic-validation")
}

p.options.UpdateResource(p.resource, p.config)

if err := p.resource.Validate(); err != nil {
Expand Down
Loading
Loading