diff --git a/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
index 1f58ce66db3..f923176b2e3 100644
--- a/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
+++ b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
@@ -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
diff --git a/docs/book/src/cronjob-tutorial/webhook-implementation.md b/docs/book/src/cronjob-tutorial/webhook-implementation.md
index 423f27f73b1..ea4b4375a42 100644
--- a/docs/book/src/cronjob-tutorial/webhook-implementation.md
+++ b/docs/book/src/cronjob-tutorial/webhook-implementation.md
@@ -11,7 +11,7 @@ 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). 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):
+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
@@ -19,4 +19,29 @@ kubebuilder create webhook --group batch --version v1 --kind CronJob --defaultin
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/`.
+
+
+Version Requirements
+
+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`).
+
+
+
{{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}}
diff --git a/docs/book/src/multiversion-tutorial/conversion.md b/docs/book/src/multiversion-tutorial/conversion.md
index 39b37a907b2..4166992e419 100644
--- a/docs/book/src/multiversion-tutorial/conversion.md
+++ b/docs/book/src/multiversion-tutorial/conversion.md
@@ -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.
+
+Conversion Webhooks and Custom Paths
+
+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.
+
+
## Hub...
First, we'll implement the hub. We'll choose the v1 version as the hub:
diff --git a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
index 2a076ab4f23..7f5224e5581 100644
--- a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
+++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go
@@ -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
diff --git a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go
index ab1d46b2f79..9d6e3d48d71 100644
--- a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go
+++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go
@@ -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
diff --git a/docs/book/src/reference/admission-webhook.md b/docs/book/src/reference/admission-webhook.md
index f37e4d47bc3..222d49f7437 100644
--- a/docs/book/src/reference/admission-webhook.md
+++ b/docs/book/src/reference/admission-webhook.md
@@ -30,6 +30,37 @@ object after your validation has accepted it.
+## 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
+```
+
+
+Version Requirements
+
+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.
+
+
+
## Handling Resource Status in Admission Webhooks
diff --git a/hack/docs/internal/cronjob-tutorial/generate_cronjob.go b/hack/docs/internal/cronjob-tutorial/generate_cronjob.go
index 7503d10761c..a15bca243b8 100644
--- a/hack/docs/internal/cronjob-tutorial/generate_cronjob.go
+++ b/hack/docs/internal/cronjob-tutorial/generate_cronjob.go
@@ -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"),
diff --git a/hack/docs/internal/cronjob-tutorial/webhook_implementation.go b/hack/docs/internal/cronjob-tutorial/webhook_implementation.go
index ec6bfdd3d53..79875195891 100644
--- a/hack/docs/internal/cronjob-tutorial/webhook_implementation.go
+++ b/hack/docs/internal/cronjob-tutorial/webhook_implementation.go
@@ -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.
diff --git a/pkg/cli/alpha/internal/generate.go b/pkg/cli/alpha/internal/generate.go
index c6dc9606417..d01f57ed1ff 100644
--- a/pkg/cli/alpha/internal/generate.go
+++ b/pkg/cli/alpha/internal/generate.go
@@ -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")
@@ -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
}
diff --git a/pkg/model/resource/webhooks.go b/pkg/model/resource/webhooks.go
index 81bab17764c..b7fc25811a0 100644
--- a/pkg/model/resource/webhooks.go
+++ b/pkg/model/resource/webhooks.go
@@ -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.
@@ -73,6 +81,8 @@ func (webhooks Webhooks) Copy() Webhooks {
Validation: webhooks.Validation,
Conversion: webhooks.Conversion,
Spoke: spokeCopy,
+ DefaultingPath: webhooks.DefaultingPath,
+ ValidationPath: webhooks.ValidationPath,
}
}
@@ -114,6 +124,14 @@ 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
}
@@ -121,7 +139,8 @@ func (webhooks *Webhooks) Update(other *Webhooks) error {
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.
diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go
index 6c8b0c8b914..0b28536a3ee 100644
--- a/pkg/plugins/golang/options.go
+++ b/pkg/plugins/golang/options.go
@@ -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
@@ -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
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 26a0b0f0f61..d7747295c3d 100644
--- a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go
@@ -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()
}
@@ -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()
}
@@ -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
@@ -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
diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go
index 8e70df13242..c51b5643022 100644
--- a/pkg/plugins/golang/v4/webhook.go
+++ b/pkg/plugins/golang/v4/webhook.go
@@ -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)
}
@@ -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). "+
@@ -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 {
diff --git a/test/e2e/v4/generate_test.go b/test/e2e/v4/generate_test.go
index 0f29b12c2c5..7cf54f7bf07 100644
--- a/test/e2e/v4/generate_test.go
+++ b/test/e2e/v4/generate_test.go
@@ -18,6 +18,7 @@ package v4
import (
"fmt"
+ "os"
"path/filepath"
"strings"
@@ -176,6 +177,69 @@ func GenerateV4WithoutWebhooks(kbc *utils.TestContext) {
"#- ../prometheus", "#")).To(Succeed())
}
+// GenerateV4WithCustomWebhookPath tests webhooks with custom paths
+func GenerateV4WithCustomWebhookPath(kbc *utils.TestContext) {
+ initingTheProject(kbc)
+ creatingAPI(kbc)
+
+ By("scaffolding both defaulting and validation webhooks with different custom paths")
+ err := kbc.CreateWebhook(
+ "--group", kbc.Group,
+ "--version", kbc.Version,
+ "--kind", kbc.Kind,
+ "--defaulting",
+ "--programmatic-validation",
+ "--defaulting-path=/custom-mutate-path",
+ "--validation-path=/custom-validate-path",
+ "--make=false",
+ )
+ Expect(err).NotTo(HaveOccurred(), "Failed to scaffold webhooks with custom paths")
+
+ By("verifying custom webhook paths in generated webhook file")
+ webhookFilePath := filepath.Join(
+ kbc.Dir, "internal/webhook", kbc.Version,
+ fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))
+
+ // Read the webhook file and check if both custom paths are present
+ content, err := os.ReadFile(webhookFilePath)
+ Expect(err).NotTo(HaveOccurred(), "Failed to read webhook file")
+ Expect(string(content)).To(ContainSubstring("path=/custom-mutate-path"),
+ "Webhook file should contain custom defaulting path")
+ Expect(string(content)).To(ContainSubstring("path=/custom-validate-path"),
+ "Webhook file should contain custom validation path")
+
+ By("implementing the webhooks")
+ err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
+ Expect(err).NotTo(HaveOccurred(), "Failed to implement webhook")
+
+ scaffoldConversionWebhook(kbc)
+ ExpectWithOffset(1, pluginutil.UncommentCode(
+ filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
+ "#- ../prometheus", "#")).To(Succeed())
+
+ By("verifying that --defaulting-path requires --defaulting flag")
+ err = kbc.CreateWebhook(
+ "--group", kbc.Group,
+ "--version", kbc.Version,
+ "--kind", "InvalidTest",
+ "--defaulting-path=/invalid-path",
+ "--make=false",
+ )
+ Expect(err).To(HaveOccurred(), "Should fail when --defaulting-path is used without --defaulting")
+ Expect(err.Error()).To(ContainSubstring("--defaulting-path can only be used with --defaulting"))
+
+ By("verifying that --validation-path requires --programmatic-validation flag")
+ err = kbc.CreateWebhook(
+ "--group", kbc.Group,
+ "--version", kbc.Version,
+ "--kind", "InvalidTest",
+ "--validation-path=/invalid-path",
+ "--make=false",
+ )
+ Expect(err).To(HaveOccurred(), "Should fail when --validation-path is used without --programmatic-validation")
+ Expect(err.Error()).To(ContainSubstring("--validation-path can only be used with --programmatic-validation"))
+}
+
func creatingAPI(kbc *utils.TestContext) {
By("creating API definition")
err := kbc.CreateAPI(
diff --git a/test/e2e/v4/plugin_cluster_test.go b/test/e2e/v4/plugin_cluster_test.go
index 14940eca73f..b307b853a4d 100644
--- a/test/e2e/v4/plugin_cluster_test.go
+++ b/test/e2e/v4/plugin_cluster_test.go
@@ -94,6 +94,10 @@ var _ = Describe("kubebuilder", func() {
GenerateV4WithoutWebhooks(kbc)
Run(kbc, false, false, false, true, false)
})
+ It("should generate a runnable project with custom webhook paths", func() {
+ GenerateV4WithCustomWebhookPath(kbc)
+ Run(kbc, true, false, false, true, false)
+ })
})
})
diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh
index 09b9c167854..f08bdf2662c 100755
--- a/test/testdata/generate.sh
+++ b/test/testdata/generate.sh
@@ -47,8 +47,13 @@ function scaffold_test_project {
$kb create api --group crew --version v2 --kind FirstMate --controller=false --resource=true --make=false
$kb create webhook --group crew --version v1 --kind FirstMate --conversion --make=false --spoke v2
+ # Create API with custom webhook paths (both defaulting and validation with different paths)
+ $kb create api --group crew --version v1 --kind Sailor --controller=true --resource=true --make=false
+ $kb create webhook --group crew --version v1 --kind Sailor --defaulting --programmatic-validation --defaulting-path=/custom-mutate-sailor --validation-path=/custom-validate-sailor --make=false
+
$kb create api --group crew --version v1 --kind Admiral --plural=admirales --controller=true --resource=true --namespaced=false --make=false
- $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting
+ # Test defaulting without custom path and validation with custom path
+ $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting --programmatic-validation --validation-path=/custom-validate-admiral
# Controller for External types
$kb create api --group "cert-manager" --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io
# Webhook for External types
diff --git a/testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook.go b/testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook.go
index 2fbc30f48e0..952e91b339a 100644
--- a/testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook.go
+++ b/testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook.go
@@ -70,8 +70,7 @@ func (d *DeploymentCustomDefaulter) Default(_ context.Context, obj runtime.Objec
}
// 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-apps-v1-deployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=vdeployment-v1.kb.io,admissionReviewVersions=v1
// DeploymentCustomValidator struct is responsible for validating the Deployment resource
diff --git a/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go
index 51b9182a859..83cd1250715 100644
--- a/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go
+++ b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go
@@ -42,8 +42,7 @@ func SetupPodWebhookWithManager(mgr ctrl.Manager) error {
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// 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--v1-pod,mutating=false,failurePolicy=fail,sideEffects=None,groups="",resources=pods,verbs=create;update,versions=v1,name=vpod-v1.kb.io,admissionReviewVersions=v1
// PodCustomValidator struct is responsible for validating the Pod resource
diff --git a/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go
index 9becc8e8e05..f0577313a37 100644
--- a/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go
+++ b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go
@@ -71,8 +71,7 @@ func (d *CaptainCustomDefaulter) 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-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1
// CaptainCustomValidator struct is responsible for validating the Captain resource
diff --git a/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go
index 19507ce9b01..47c9dc227b7 100644
--- a/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go
+++ b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go
@@ -43,8 +43,7 @@ func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error {
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// 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-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1
// MemcachedCustomValidator struct is responsible for validating the Memcached resource
diff --git a/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go
index b88b9d9287f..4b6f4d5536a 100644
--- a/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go
+++ b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go
@@ -43,8 +43,7 @@ func SetupCruiserWebhookWithManager(mgr ctrl.Manager) error {
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// 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-ship-testproject-org-v2alpha1-cruiser,mutating=false,failurePolicy=fail,sideEffects=None,groups=ship.testproject.org,resources=cruisers,verbs=create;update,versions=v2alpha1,name=vcruiser-v2alpha1.kb.io,admissionReviewVersions=v1
// CruiserCustomValidator struct is responsible for validating the Cruiser resource
diff --git a/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go
index f84e6ffc441..c354b88bc10 100644
--- a/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go
+++ b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go
@@ -43,8 +43,7 @@ func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error {
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// 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-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1
// MemcachedCustomValidator struct is responsible for validating the Memcached resource
diff --git a/testdata/project-v4/PROJECT b/testdata/project-v4/PROJECT
index 608fd538064..5051e2da0c0 100644
--- a/testdata/project-v4/PROJECT
+++ b/testdata/project-v4/PROJECT
@@ -44,6 +44,21 @@ resources:
kind: FirstMate
path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v2
version: v2
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: testproject.org
+ group: crew
+ kind: Sailor
+ path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1
+ version: v1
+ webhooks:
+ defaulting: true
+ defaultingPath: /custom-mutate-sailor
+ validation: true
+ validationPath: /custom-validate-sailor
+ webhookVersion: v1
- api:
crdVersion: v1
controller: true
@@ -55,6 +70,8 @@ resources:
version: v1
webhooks:
defaulting: true
+ validation: true
+ validationPath: /custom-validate-admiral
webhookVersion: v1
- controller: true
domain: io
diff --git a/testdata/project-v4/api/v1/sailor_types.go b/testdata/project-v4/api/v1/sailor_types.go
new file mode 100644
index 00000000000..b84cb0b2260
--- /dev/null
+++ b/testdata/project-v4/api/v1/sailor_types.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2025 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
+
+// SailorSpec defines the desired state of Sailor
+type SailorSpec struct {
+ // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
+ // Important: Run "make" to regenerate code after modifying this file
+ // The following markers will use OpenAPI v3 schema to validate the value
+ // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html
+
+ // foo is an example field of Sailor. Edit sailor_types.go to remove/update
+ // +optional
+ Foo *string `json:"foo,omitempty"`
+}
+
+// SailorStatus defines the observed state of Sailor.
+type SailorStatus struct {
+ // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+ // Important: Run "make" to regenerate code after modifying this file
+
+ // For Kubernetes API conventions, see:
+ // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
+
+ // conditions represent the current state of the Sailor resource.
+ // Each condition has a unique type and reflects the status of a specific aspect of the resource.
+ //
+ // Standard condition types include:
+ // - "Available": the resource is fully functional
+ // - "Progressing": the resource is being created or updated
+ // - "Degraded": the resource failed to reach or maintain its desired state
+ //
+ // The status of each condition is one of True, False, or Unknown.
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+
+// Sailor is the Schema for the sailors API
+type Sailor struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // metadata is a standard object metadata
+ // +optional
+ metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`
+
+ // spec defines the desired state of Sailor
+ // +required
+ Spec SailorSpec `json:"spec"`
+
+ // status defines the observed state of Sailor
+ // +optional
+ Status SailorStatus `json:"status,omitempty,omitzero"`
+}
+
+// +kubebuilder:object:root=true
+
+// SailorList contains a list of Sailor
+type SailorList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []Sailor `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&Sailor{}, &SailorList{})
+}
diff --git a/testdata/project-v4/api/v1/zz_generated.deepcopy.go b/testdata/project-v4/api/v1/zz_generated.deepcopy.go
index 2035c89718b..8291d0e43ec 100644
--- a/testdata/project-v4/api/v1/zz_generated.deepcopy.go
+++ b/testdata/project-v4/api/v1/zz_generated.deepcopy.go
@@ -327,3 +327,104 @@ func (in *FirstMateStatus) DeepCopy() *FirstMateStatus {
in.DeepCopyInto(out)
return out
}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Sailor) DeepCopyInto(out *Sailor) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sailor.
+func (in *Sailor) DeepCopy() *Sailor {
+ if in == nil {
+ return nil
+ }
+ out := new(Sailor)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Sailor) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SailorList) DeepCopyInto(out *SailorList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]Sailor, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorList.
+func (in *SailorList) DeepCopy() *SailorList {
+ if in == nil {
+ return nil
+ }
+ out := new(SailorList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *SailorList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SailorSpec) DeepCopyInto(out *SailorSpec) {
+ *out = *in
+ if in.Foo != nil {
+ in, out := &in.Foo, &out.Foo
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorSpec.
+func (in *SailorSpec) DeepCopy() *SailorSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(SailorSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SailorStatus) DeepCopyInto(out *SailorStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]metav1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorStatus.
+func (in *SailorStatus) DeepCopy() *SailorStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(SailorStatus)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/testdata/project-v4/cmd/main.go b/testdata/project-v4/cmd/main.go
index b347e30e664..9ee1b900992 100644
--- a/testdata/project-v4/cmd/main.go
+++ b/testdata/project-v4/cmd/main.go
@@ -212,6 +212,20 @@ func main() {
os.Exit(1)
}
}
+ if err := (&controller.SailorReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "Sailor")
+ os.Exit(1)
+ }
+ // nolint:goconst
+ if os.Getenv("ENABLE_WEBHOOKS") != "false" {
+ if err := webhookv1.SetupSailorWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "Sailor")
+ os.Exit(1)
+ }
+ }
if err := (&controller.AdmiralReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
diff --git a/testdata/project-v4/config/crd/bases/crew.testproject.org_sailors.yaml b/testdata/project-v4/config/crd/bases/crew.testproject.org_sailors.yaml
new file mode 100644
index 00000000000..3aed6231d5c
--- /dev/null
+++ b/testdata/project-v4/config/crd/bases/crew.testproject.org_sailors.yaml
@@ -0,0 +1,126 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.19.0
+ name: sailors.crew.testproject.org
+spec:
+ group: crew.testproject.org
+ names:
+ kind: Sailor
+ listKind: SailorList
+ plural: sailors
+ singular: sailor
+ scope: Namespaced
+ versions:
+ - name: v1
+ schema:
+ openAPIV3Schema:
+ description: Sailor is the Schema for the sailors API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: spec defines the desired state of Sailor
+ properties:
+ foo:
+ description: foo is an example field of Sailor. Edit sailor_types.go
+ to remove/update
+ type: string
+ type: object
+ status:
+ description: status defines the observed state of Sailor
+ properties:
+ conditions:
+ description: |-
+ conditions represent the current state of the Sailor resource.
+ Each condition has a unique type and reflects the status of a specific aspect of the resource.
+
+ Standard condition types include:
+ - "Available": the resource is fully functional
+ - "Progressing": the resource is being created or updated
+ - "Degraded": the resource failed to reach or maintain its desired state
+
+ The status of each condition is one of True, False, or Unknown.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/testdata/project-v4/config/crd/kustomization.yaml b/testdata/project-v4/config/crd/kustomization.yaml
index 2364696ece4..fb8a418e3aa 100644
--- a/testdata/project-v4/config/crd/kustomization.yaml
+++ b/testdata/project-v4/config/crd/kustomization.yaml
@@ -4,6 +4,7 @@
resources:
- bases/crew.testproject.org_captains.yaml
- bases/crew.testproject.org_firstmates.yaml
+- bases/crew.testproject.org_sailors.yaml
- bases/crew.testproject.org_admirales.yaml
# +kubebuilder:scaffold:crdkustomizeresource
diff --git a/testdata/project-v4/config/rbac/kustomization.yaml b/testdata/project-v4/config/rbac/kustomization.yaml
index 6aef64a7bba..07f0e0c05e5 100644
--- a/testdata/project-v4/config/rbac/kustomization.yaml
+++ b/testdata/project-v4/config/rbac/kustomization.yaml
@@ -25,6 +25,9 @@ resources:
- admiral_admin_role.yaml
- admiral_editor_role.yaml
- admiral_viewer_role.yaml
+- sailor_admin_role.yaml
+- sailor_editor_role.yaml
+- sailor_viewer_role.yaml
- firstmate_admin_role.yaml
- firstmate_editor_role.yaml
- firstmate_viewer_role.yaml
diff --git a/testdata/project-v4/config/rbac/role.yaml b/testdata/project-v4/config/rbac/role.yaml
index dfa4ad22cab..44231645b56 100644
--- a/testdata/project-v4/config/rbac/role.yaml
+++ b/testdata/project-v4/config/rbac/role.yaml
@@ -36,6 +36,7 @@ rules:
- admirales
- captains
- firstmates
+ - sailors
verbs:
- create
- delete
@@ -50,6 +51,7 @@ rules:
- admirales/finalizers
- captains/finalizers
- firstmates/finalizers
+ - sailors/finalizers
verbs:
- update
- apiGroups:
@@ -58,6 +60,7 @@ rules:
- admirales/status
- captains/status
- firstmates/status
+ - sailors/status
verbs:
- get
- patch
diff --git a/testdata/project-v4/config/rbac/sailor_admin_role.yaml b/testdata/project-v4/config/rbac/sailor_admin_role.yaml
new file mode 100644
index 00000000000..6899b50a705
--- /dev/null
+++ b/testdata/project-v4/config/rbac/sailor_admin_role.yaml
@@ -0,0 +1,27 @@
+# This rule is not used by the project project-v4 itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over crew.testproject.org.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: project-v4
+ app.kubernetes.io/managed-by: kustomize
+ name: sailor-admin-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - '*'
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
diff --git a/testdata/project-v4/config/rbac/sailor_editor_role.yaml b/testdata/project-v4/config/rbac/sailor_editor_role.yaml
new file mode 100644
index 00000000000..22138db4c62
--- /dev/null
+++ b/testdata/project-v4/config/rbac/sailor_editor_role.yaml
@@ -0,0 +1,33 @@
+# This rule is not used by the project project-v4 itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants permissions to create, update, and delete resources within the crew.testproject.org.
+# This role is intended for users who need to manage these resources
+# but should not control RBAC or manage permissions for others.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: project-v4
+ app.kubernetes.io/managed-by: kustomize
+ name: sailor-editor-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
diff --git a/testdata/project-v4/config/rbac/sailor_viewer_role.yaml b/testdata/project-v4/config/rbac/sailor_viewer_role.yaml
new file mode 100644
index 00000000000..ac0e9e2bf87
--- /dev/null
+++ b/testdata/project-v4/config/rbac/sailor_viewer_role.yaml
@@ -0,0 +1,29 @@
+# This rule is not used by the project project-v4 itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to crew.testproject.org resources.
+# This role is intended for users who need visibility into these resources
+# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: project-v4
+ app.kubernetes.io/managed-by: kustomize
+ name: sailor-viewer-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
diff --git a/testdata/project-v4/config/samples/crew_v1_sailor.yaml b/testdata/project-v4/config/samples/crew_v1_sailor.yaml
new file mode 100644
index 00000000000..0adf1d32f72
--- /dev/null
+++ b/testdata/project-v4/config/samples/crew_v1_sailor.yaml
@@ -0,0 +1,9 @@
+apiVersion: crew.testproject.org/v1
+kind: Sailor
+metadata:
+ labels:
+ app.kubernetes.io/name: project-v4
+ app.kubernetes.io/managed-by: kustomize
+ name: sailor-sample
+spec:
+ # TODO(user): Add fields here
diff --git a/testdata/project-v4/config/samples/kustomization.yaml b/testdata/project-v4/config/samples/kustomization.yaml
index ce83fd55d0c..e8ab3fe39ac 100644
--- a/testdata/project-v4/config/samples/kustomization.yaml
+++ b/testdata/project-v4/config/samples/kustomization.yaml
@@ -3,5 +3,6 @@ resources:
- crew_v1_captain.yaml
- crew_v1_firstmate.yaml
- crew_v2_firstmate.yaml
+- crew_v1_sailor.yaml
- crew_v1_admiral.yaml
# +kubebuilder:scaffold:manifestskustomizesamples
diff --git a/testdata/project-v4/config/webhook/manifests.yaml b/testdata/project-v4/config/webhook/manifests.yaml
index 56d49ae0df1..a375773dd49 100644
--- a/testdata/project-v4/config/webhook/manifests.yaml
+++ b/testdata/project-v4/config/webhook/manifests.yaml
@@ -104,12 +104,52 @@ webhooks:
resources:
- pods
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /custom-mutate-sailor
+ failurePolicy: Fail
+ name: msailor-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - sailors
+ sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /custom-validate-admiral
+ failurePolicy: Fail
+ name: vadmiral-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - admirales
+ sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
@@ -150,3 +190,23 @@ webhooks:
resources:
- deployments
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /custom-validate-sailor
+ failurePolicy: Fail
+ name: vsailor-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - sailors
+ sideEffects: None
diff --git a/testdata/project-v4/dist/install.yaml b/testdata/project-v4/dist/install.yaml
index 2019455452c..7286a626721 100644
--- a/testdata/project-v4/dist/install.yaml
+++ b/testdata/project-v4/dist/install.yaml
@@ -506,6 +506,132 @@ spec:
subresources:
status: {}
---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.19.0
+ name: sailors.crew.testproject.org
+spec:
+ group: crew.testproject.org
+ names:
+ kind: Sailor
+ listKind: SailorList
+ plural: sailors
+ singular: sailor
+ scope: Namespaced
+ versions:
+ - name: v1
+ schema:
+ openAPIV3Schema:
+ description: Sailor is the Schema for the sailors API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: spec defines the desired state of Sailor
+ properties:
+ foo:
+ description: foo is an example field of Sailor. Edit sailor_types.go
+ to remove/update
+ type: string
+ type: object
+ status:
+ description: status defines the observed state of Sailor
+ properties:
+ conditions:
+ description: |-
+ conditions represent the current state of the Sailor resource.
+ Each condition has a unique type and reflects the status of a specific aspect of the resource.
+
+ Standard condition types include:
+ - "Available": the resource is fully functional
+ - "Progressing": the resource is being created or updated
+ - "Degraded": the resource failed to reach or maintain its desired state
+
+ The status of each condition is one of True, False, or Unknown.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+---
apiVersion: v1
kind: ServiceAccount
metadata:
@@ -806,6 +932,7 @@ rules:
- admirales
- captains
- firstmates
+ - sailors
verbs:
- create
- delete
@@ -820,6 +947,7 @@ rules:
- admirales/finalizers
- captains/finalizers
- firstmates/finalizers
+ - sailors/finalizers
verbs:
- update
- apiGroups:
@@ -828,6 +956,7 @@ rules:
- admirales/status
- captains/status
- firstmates/status
+ - sailors/status
verbs:
- get
- patch
@@ -862,6 +991,77 @@ rules:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/name: project-v4
+ name: project-v4-sailor-admin-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - '*'
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/name: project-v4
+ name: project-v4-sailor-editor-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/name: project-v4
+ name: project-v4-sailor-viewer-role
+rules:
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - crew.testproject.org
+ resources:
+ - sailors/status
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
@@ -1172,6 +1372,26 @@ webhooks:
resources:
- pods
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: project-v4-webhook-service
+ namespace: project-v4-system
+ path: /custom-mutate-sailor
+ failurePolicy: Fail
+ name: msailor-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - sailors
+ sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
@@ -1180,6 +1400,26 @@ metadata:
cert-manager.io/inject-ca-from: project-v4-system/project-v4-serving-cert
name: project-v4-validating-webhook-configuration
webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: project-v4-webhook-service
+ namespace: project-v4-system
+ path: /custom-validate-admiral
+ failurePolicy: Fail
+ name: vadmiral-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - admirales
+ sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
@@ -1220,3 +1460,23 @@ webhooks:
resources:
- deployments
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: project-v4-webhook-service
+ namespace: project-v4-system
+ path: /custom-validate-sailor
+ failurePolicy: Fail
+ name: vsailor-v1.kb.io
+ rules:
+ - apiGroups:
+ - crew.testproject.org
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - sailors
+ sideEffects: None
diff --git a/testdata/project-v4/internal/controller/sailor_controller.go b/testdata/project-v4/internal/controller/sailor_controller.go
new file mode 100644
index 00000000000..872039e7874
--- /dev/null
+++ b/testdata/project-v4/internal/controller/sailor_controller.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2025 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+ crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
+)
+
+// SailorReconciler reconciles a Sailor object
+type SailorReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+}
+
+// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the Sailor object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.3/pkg/reconcile
+func (r *SailorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ _ = logf.FromContext(ctx)
+
+ // TODO(user): your logic here
+
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *SailorReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&crewv1.Sailor{}).
+ Named("sailor").
+ Complete(r)
+}
diff --git a/testdata/project-v4/internal/controller/sailor_controller_test.go b/testdata/project-v4/internal/controller/sailor_controller_test.go
new file mode 100644
index 00000000000..6829e53a88a
--- /dev/null
+++ b/testdata/project-v4/internal/controller/sailor_controller_test.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2025 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+ "context"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
+)
+
+var _ = Describe("Sailor Controller", func() {
+ Context("When reconciling a resource", func() {
+ const resourceName = "test-resource"
+
+ ctx := context.Background()
+
+ typeNamespacedName := types.NamespacedName{
+ Name: resourceName,
+ Namespace: "default", // TODO(user):Modify as needed
+ }
+ sailor := &crewv1.Sailor{}
+
+ BeforeEach(func() {
+ By("creating the custom resource for the Kind Sailor")
+ err := k8sClient.Get(ctx, typeNamespacedName, sailor)
+ if err != nil && errors.IsNotFound(err) {
+ resource := &crewv1.Sailor{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: resourceName,
+ Namespace: "default",
+ },
+ // TODO(user): Specify other spec details if needed.
+ }
+ Expect(k8sClient.Create(ctx, resource)).To(Succeed())
+ }
+ })
+
+ AfterEach(func() {
+ // TODO(user): Cleanup logic after each test, like removing the resource instance.
+ resource := &crewv1.Sailor{}
+ err := k8sClient.Get(ctx, typeNamespacedName, resource)
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Cleanup the specific resource instance Sailor")
+ Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
+ })
+ It("should successfully reconcile the resource", func() {
+ By("Reconciling the created resource")
+ controllerReconciler := &SailorReconciler{
+ Client: k8sClient,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+ Expect(err).NotTo(HaveOccurred())
+ // TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
+ // Example: If you expect a certain status condition after reconciliation, verify it here.
+ })
+ })
+})
diff --git a/testdata/project-v4/internal/webhook/v1/admiral_webhook.go b/testdata/project-v4/internal/webhook/v1/admiral_webhook.go
index a30f4394364..f7ca94c1e61 100644
--- a/testdata/project-v4/internal/webhook/v1/admiral_webhook.go
+++ b/testdata/project-v4/internal/webhook/v1/admiral_webhook.go
@@ -24,6 +24,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)
@@ -35,6 +36,8 @@ var admirallog = logf.Log.WithName("admiral-resource")
// SetupAdmiralWebhookWithManager registers the webhook for Admiral in the manager.
func SetupAdmiralWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.Admiral{}).
+ WithValidator(&AdmiralCustomValidator{}).
+ WithValidatorCustomPath("/custom-validate-admiral").
WithDefaulter(&AdmiralCustomDefaulter{}).
Complete()
}
@@ -67,3 +70,57 @@ func (d *AdmiralCustomDefaulter) Default(_ context.Context, obj runtime.Object)
return nil
}
+
+// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
+// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
+// +kubebuilder:webhook:path=/custom-validate-admiral,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=admirales,verbs=create;update,versions=v1,name=vadmiral-v1.kb.io,admissionReviewVersions=v1
+
+// AdmiralCustomValidator struct is responsible for validating the Admiral resource
+// when it is created, updated, or deleted.
+//
+// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
+// as this struct is used only for temporary operations and does not need to be deeply copied.
+type AdmiralCustomValidator struct {
+ // TODO(user): Add more fields as needed for validation
+}
+
+var _ webhook.CustomValidator = &AdmiralCustomValidator{}
+
+// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
+func (v *AdmiralCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
+ admiral, ok := obj.(*crewv1.Admiral)
+ if !ok {
+ return nil, fmt.Errorf("expected a Admiral object but got %T", obj)
+ }
+ admirallog.Info("Validation for Admiral upon creation", "name", admiral.GetName())
+
+ // TODO(user): fill in your validation logic upon object creation.
+
+ return nil, nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
+func (v *AdmiralCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+ admiral, ok := newObj.(*crewv1.Admiral)
+ if !ok {
+ return nil, fmt.Errorf("expected a Admiral object for the newObj but got %T", newObj)
+ }
+ admirallog.Info("Validation for Admiral upon update", "name", admiral.GetName())
+
+ // TODO(user): fill in your validation logic upon object update.
+
+ return nil, nil
+}
+
+// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
+func (v *AdmiralCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+ admiral, ok := obj.(*crewv1.Admiral)
+ if !ok {
+ return nil, fmt.Errorf("expected a Admiral object but got %T", obj)
+ }
+ admirallog.Info("Validation for Admiral upon deletion", "name", admiral.GetName())
+
+ // TODO(user): fill in your validation logic upon object deletion.
+
+ return nil, nil
+}
diff --git a/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go b/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go
index 98d28a4f723..f19407de9ad 100644
--- a/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go
+++ b/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go
@@ -28,12 +28,15 @@ var _ = Describe("Admiral Webhook", func() {
var (
obj *crewv1.Admiral
oldObj *crewv1.Admiral
+ validator AdmiralCustomValidator
defaulter AdmiralCustomDefaulter
)
BeforeEach(func() {
obj = &crewv1.Admiral{}
oldObj = &crewv1.Admiral{}
+ validator = AdmiralCustomValidator{}
+ Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
defaulter = AdmiralCustomDefaulter{}
Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
@@ -58,4 +61,27 @@ var _ = Describe("Admiral Webhook", func() {
// })
})
+ Context("When creating or updating Admiral under Validating Webhook", func() {
+ // TODO (user): Add logic for validating webhooks
+ // Example:
+ // It("Should deny creation if a required field is missing", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = ""
+ // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
+ // })
+ //
+ // It("Should admit creation if all required fields are present", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = "valid_value"
+ // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
+ // })
+ //
+ // It("Should validate updates correctly", func() {
+ // By("simulating a valid update scenario")
+ // oldObj.SomeRequiredField = "updated_value"
+ // obj.SomeRequiredField = "updated_value"
+ // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
+ // })
+ })
+
})
diff --git a/testdata/project-v4/internal/webhook/v1/captain_webhook.go b/testdata/project-v4/internal/webhook/v1/captain_webhook.go
index f6d80fb6562..9783251ad60 100644
--- a/testdata/project-v4/internal/webhook/v1/captain_webhook.go
+++ b/testdata/project-v4/internal/webhook/v1/captain_webhook.go
@@ -71,8 +71,7 @@ func (d *CaptainCustomDefaulter) 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-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1
// CaptainCustomValidator struct is responsible for validating the Captain resource
diff --git a/testdata/project-v4/internal/webhook/v1/deployment_webhook.go b/testdata/project-v4/internal/webhook/v1/deployment_webhook.go
index 2fbc30f48e0..952e91b339a 100644
--- a/testdata/project-v4/internal/webhook/v1/deployment_webhook.go
+++ b/testdata/project-v4/internal/webhook/v1/deployment_webhook.go
@@ -70,8 +70,7 @@ func (d *DeploymentCustomDefaulter) Default(_ context.Context, obj runtime.Objec
}
// 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-apps-v1-deployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=vdeployment-v1.kb.io,admissionReviewVersions=v1
// DeploymentCustomValidator struct is responsible for validating the Deployment resource
diff --git a/testdata/project-v4/internal/webhook/v1/sailor_webhook.go b/testdata/project-v4/internal/webhook/v1/sailor_webhook.go
new file mode 100644
index 00000000000..700937aa77a
--- /dev/null
+++ b/testdata/project-v4/internal/webhook/v1/sailor_webhook.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2025 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1
+
+import (
+ "context"
+ "fmt"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
+)
+
+// nolint:unused
+// log is for logging in this package.
+var sailorlog = logf.Log.WithName("sailor-resource")
+
+// SetupSailorWebhookWithManager registers the webhook for Sailor in the manager.
+func SetupSailorWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.Sailor{}).
+ WithValidator(&SailorCustomValidator{}).
+ WithValidatorCustomPath("/custom-validate-sailor").
+ WithDefaulter(&SailorCustomDefaulter{}).
+ WithDefaulterCustomPath("/custom-mutate-sailor").
+ Complete()
+}
+
+// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+
+// +kubebuilder:webhook:path=/custom-mutate-sailor,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=sailors,verbs=create;update,versions=v1,name=msailor-v1.kb.io,admissionReviewVersions=v1
+
+// SailorCustomDefaulter struct is responsible for setting default values on the custom resource of the
+// Kind Sailor when those are created or updated.
+//
+// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
+// as it is used only for temporary operations and does not need to be deeply copied.
+type SailorCustomDefaulter struct {
+ // TODO(user): Add more fields as needed for defaulting
+}
+
+var _ webhook.CustomDefaulter = &SailorCustomDefaulter{}
+
+// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Sailor.
+func (d *SailorCustomDefaulter) Default(_ context.Context, obj runtime.Object) error {
+ sailor, ok := obj.(*crewv1.Sailor)
+
+ if !ok {
+ return fmt.Errorf("expected an Sailor object but got %T", obj)
+ }
+ sailorlog.Info("Defaulting for Sailor", "name", sailor.GetName())
+
+ // TODO(user): fill in your defaulting logic.
+
+ return nil
+}
+
+// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
+// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
+// +kubebuilder:webhook:path=/custom-validate-sailor,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=sailors,verbs=create;update,versions=v1,name=vsailor-v1.kb.io,admissionReviewVersions=v1
+
+// SailorCustomValidator struct is responsible for validating the Sailor resource
+// when it is created, updated, or deleted.
+//
+// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
+// as this struct is used only for temporary operations and does not need to be deeply copied.
+type SailorCustomValidator struct {
+ // TODO(user): Add more fields as needed for validation
+}
+
+var _ webhook.CustomValidator = &SailorCustomValidator{}
+
+// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
+func (v *SailorCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
+ sailor, ok := obj.(*crewv1.Sailor)
+ if !ok {
+ return nil, fmt.Errorf("expected a Sailor object but got %T", obj)
+ }
+ sailorlog.Info("Validation for Sailor upon creation", "name", sailor.GetName())
+
+ // TODO(user): fill in your validation logic upon object creation.
+
+ return nil, nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
+func (v *SailorCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+ sailor, ok := newObj.(*crewv1.Sailor)
+ if !ok {
+ return nil, fmt.Errorf("expected a Sailor object for the newObj but got %T", newObj)
+ }
+ sailorlog.Info("Validation for Sailor upon update", "name", sailor.GetName())
+
+ // TODO(user): fill in your validation logic upon object update.
+
+ return nil, nil
+}
+
+// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
+func (v *SailorCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+ sailor, ok := obj.(*crewv1.Sailor)
+ if !ok {
+ return nil, fmt.Errorf("expected a Sailor object but got %T", obj)
+ }
+ sailorlog.Info("Validation for Sailor upon deletion", "name", sailor.GetName())
+
+ // TODO(user): fill in your validation logic upon object deletion.
+
+ return nil, nil
+}
diff --git a/testdata/project-v4/internal/webhook/v1/sailor_webhook_test.go b/testdata/project-v4/internal/webhook/v1/sailor_webhook_test.go
new file mode 100644
index 00000000000..253d47a068f
--- /dev/null
+++ b/testdata/project-v4/internal/webhook/v1/sailor_webhook_test.go
@@ -0,0 +1,87 @@
+/*
+Copyright 2025 The Kubernetes authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
+ // TODO (user): Add any additional imports if needed
+)
+
+var _ = Describe("Sailor Webhook", func() {
+ var (
+ obj *crewv1.Sailor
+ oldObj *crewv1.Sailor
+ validator SailorCustomValidator
+ defaulter SailorCustomDefaulter
+ )
+
+ BeforeEach(func() {
+ obj = &crewv1.Sailor{}
+ oldObj = &crewv1.Sailor{}
+ validator = SailorCustomValidator{}
+ Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
+ defaulter = SailorCustomDefaulter{}
+ Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
+ Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
+ Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
+ // TODO (user): Add any setup logic common to all tests
+ })
+
+ AfterEach(func() {
+ // TODO (user): Add any teardown logic common to all tests
+ })
+
+ Context("When creating Sailor under Defaulting Webhook", func() {
+ // TODO (user): Add logic for defaulting webhooks
+ // Example:
+ // It("Should apply defaults when a required field is empty", func() {
+ // By("simulating a scenario where defaults should be applied")
+ // obj.SomeFieldWithDefault = ""
+ // By("calling the Default method to apply defaults")
+ // defaulter.Default(ctx, obj)
+ // By("checking that the default values are set")
+ // Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
+ // })
+ })
+
+ Context("When creating or updating Sailor under Validating Webhook", func() {
+ // TODO (user): Add logic for validating webhooks
+ // Example:
+ // It("Should deny creation if a required field is missing", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = ""
+ // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
+ // })
+ //
+ // It("Should admit creation if all required fields are present", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = "valid_value"
+ // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
+ // })
+ //
+ // It("Should validate updates correctly", func() {
+ // By("simulating a valid update scenario")
+ // oldObj.SomeRequiredField = "updated_value"
+ // obj.SomeRequiredField = "updated_value"
+ // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
+ // })
+ })
+
+})
diff --git a/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go
index 0294d8117c6..5f051f2bfea 100644
--- a/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go
+++ b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go
@@ -112,6 +112,9 @@ var _ = BeforeSuite(func() {
err = SetupCaptainWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())
+ err = SetupSailorWebhookWithManager(mgr)
+ Expect(err).NotTo(HaveOccurred())
+
err = SetupAdmiralWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())