From 1fb13814ac7bbed9a64568bf2f2ba28bc78c0ce4 Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Wed, 2 Oct 2024 08:53:03 +0100 Subject: [PATCH] Add Support for Scaffolding Webhooks for Core Types This update introduces support for scaffolding webhooks for Core Types, which are Kubernetes-native resources defined in the Kubernetes API. --- docs/book/src/SUMMARY.md | 1 - docs/book/src/reference/project-config.md | 45 +-- .../reference/using_an_external_resource.md | 11 +- .../src/reference/webhook-for-core-types.md | 328 ------------------ pkg/model/resource/resource.go | 3 + .../common/kustomize/v2/scaffolds/webhook.go | 2 +- pkg/plugins/golang/options.go | 1 + .../internal/templates/webhooks/webhook.go | 4 +- pkg/plugins/golang/v4/webhook.go | 10 +- test/testdata/generate.sh | 6 +- testdata/project-v4-multigroup/PROJECT | 8 + testdata/project-v4-multigroup/cmd/main.go | 8 + .../crd/patches/cainjection_in_core_pods.yaml | 7 + .../crd/patches/webhook_in_core_pods.yaml | 16 + .../config/webhook/manifests.yaml | 8 +- .../project-v4-multigroup/dist/install.yaml | 8 +- .../webhook/certmanager/v1/issuer_webhook.go | 57 --- .../certmanager/v1/issuer_webhook_test.go | 26 -- .../internal/webhook/core/v1/pod_webhook.go | 97 ++++++ .../webhook/core/v1/pod_webhook_test.go | 71 ++++ .../webhook/core/v1/webhook_suite_test.go | 149 ++++++++ testdata/project-v4/PROJECT | 8 + testdata/project-v4/cmd/main.go | 8 + .../crd/patches/cainjection_in_pods.yaml | 7 + .../config/crd/patches/webhook_in_pods.yaml | 16 + .../project-v4/config/webhook/manifests.yaml | 20 ++ testdata/project-v4/dist/install.yaml | 20 ++ .../internal/webhook/v1/pod_webhook.go | 68 ++++ .../internal/webhook/v1/pod_webhook_test.go | 61 ++++ .../internal/webhook/v1/webhook_suite_test.go | 3 + 30 files changed, 616 insertions(+), 461 deletions(-) delete mode 100644 docs/book/src/reference/webhook-for-core-types.md create mode 100644 testdata/project-v4-multigroup/config/crd/patches/cainjection_in_core_pods.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/webhook_in_core_pods.yaml create mode 100644 testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go create mode 100644 testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook_test.go create mode 100644 testdata/project-v4-multigroup/internal/webhook/core/v1/webhook_suite_test.go create mode 100644 testdata/project-v4/config/crd/patches/cainjection_in_pods.yaml create mode 100644 testdata/project-v4/config/crd/patches/webhook_in_pods.yaml create mode 100644 testdata/project-v4/internal/webhook/v1/pod_webhook.go create mode 100644 testdata/project-v4/internal/webhook/v1/pod_webhook_test.go diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 3b64b77ac3b..ae04a16678a 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -82,7 +82,6 @@ - [Kind for Dev & CI](reference/kind.md) - [What's a webhook?](reference/webhook-overview.md) - [Admission webhook](reference/admission-webhook.md) - - [Webhooks for Core Types](reference/webhook-for-core-types.md) - [Markers for Config/Code Generation](./reference/markers.md) - [CRD Generation](./reference/markers/crd.md) diff --git a/docs/book/src/reference/project-config.md b/docs/book/src/reference/project-config.md index 2b7be3bafe8..0b9faa2a536 100644 --- a/docs/book/src/reference/project-config.md +++ b/docs/book/src/reference/project-config.md @@ -130,29 +130,30 @@ version: "3" Now let's check its layout fields definition: -| Field | Description | -|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `layout` | Defines the global plugins, e.g. a project `init` with `--plugins="go/v4,deploy-image/v1-alpha"` means that any sub-command used will always call its implementation for both plugins in a chain. | -| `domain` | Store the domain of the project. This information can be provided by the user when the project is generate with the `init` sub-command and the `domain` flag. | -| `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. | -| `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. | -| `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. | -| `resources` | An array of all resources which were scaffolded in the project. | -| `resources.api` | The API scaffolded in the project via the sub-command `create api`. | -| `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. | -| `resources.api.namespaced` | The API RBAC permissions which can be namespaced or cluster scoped. | -| `resources.controller` | Indicates whether a controller was scaffolded for the API. | -| `resources.domain` | The domain of the resource which was provided by the `--domain` flag when the project was initialized or via the flag `--external-api-domain` when it was used to scaffold controllers for an [External Type][external-type]. | -| `resources.group` | The GKV group of the resource which is provided by the `--group` flag when the sub-command `create api` is used. | -| `resources.version` | The GKV version of the resource which is provided by the `--version` flag when the sub-command `create api` is used. | -| `resources.kind` | Store GKV Kind of the resource which is provided by the `--kind` flag when the sub-command `create api` is used. | +| Field | Description | +|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `layout` | Defines the global plugins, e.g. a project `init` with `--plugins="go/v4,deploy-image/v1-alpha"` means that any sub-command used will always call its implementation for both plugins in a chain. | +| `domain` | Store the domain of the project. This information can be provided by the user when the project is generate with the `init` sub-command and the `domain` flag. | +| `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. | +| `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. | +| `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. | +| `resources` | An array of all resources which were scaffolded in the project. | +| `resources.api` | The API scaffolded in the project via the sub-command `create api`. | +| `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. | +| `resources.api.namespaced` | The API RBAC permissions which can be namespaced or cluster scoped. | +| `resources.controller` | Indicates whether a controller was scaffolded for the API. | +| `resources.domain` | The domain of the resource which was provided by the `--domain` flag when the project was initialized or via the flag `--external-api-domain` when it was used to scaffold controllers for an [External Type][external-type]. | +| `resources.group` | The GKV group of the resource which is provided by the `--group` flag when the sub-command `create api` is used. | +| `resources.version` | The GKV version of the resource which is provided by the `--version` flag when the sub-command `create api` is used. | +| `resources.kind` | Store GKV Kind of the resource which is provided by the `--kind` flag when the sub-command `create api` is used. | | `resources.path` | The import path for the API resource. It will be `/api/` unless the API added to the project is an external or core-type. For the core-types scenarios, the paths used are mapped [here][core-types]. Or either the path informed by the flag `--external-api-path` | -| `resources.external` | It is `true` when the flag `--external-api-path` was used to generated the scaffold for an [External Type][external-type]. | -| `resources.webhooks` | Store the webhooks data when the sub-command `create webhook` is used. | -| `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | -| `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | -| `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | -| `resources.webhooks.validation` | It is `true` when the webhook was scaffold with the `--programmatic-validation` flag which means that is a validation webhook. | +| `resources.core` | It is `true` when the group used is from Kubernetes API and the API resource is not defined on the project. | +| `resources.external` | It is `true` when the flag `--external-api-path` was used to generated the scaffold for an [External Type][external-type]. | +| `resources.webhooks` | Store the webhooks data when the sub-command `create webhook` is used. | +| `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | +| `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | +| `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | +| `resources.webhooks.validation` | It is `true` when the webhook was scaffold with the `--programmatic-validation` flag which means that is a validation webhook. | [project]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v3/PROJECT [versioning]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#Versioning diff --git a/docs/book/src/reference/using_an_external_resource.md b/docs/book/src/reference/using_an_external_resource.md index dae069d4a17..298255c6fb8 100644 --- a/docs/book/src/reference/using_an_external_resource.md +++ b/docs/book/src/reference/using_an_external_resource.md @@ -170,11 +170,10 @@ definitions since the type is already defined in the Kubernetes API. ### Creating a Webhook to Manage a Core Type - +```go +kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation +``` -[webhook-for-core-types]: ./webhook-for-core-types.md diff --git a/docs/book/src/reference/webhook-for-core-types.md b/docs/book/src/reference/webhook-for-core-types.md deleted file mode 100644 index f2eac3e8a86..00000000000 --- a/docs/book/src/reference/webhook-for-core-types.md +++ /dev/null @@ -1,328 +0,0 @@ -# Admission Webhook for Core Types - -It is very easy to build admission webhooks for CRDs, which has been covered in -the [CronJob tutorial][cronjob-tutorial]. Given that kubebuilder doesn't support webhook scaffolding -for core types, you have to use the library from controller-runtime to handle it. -There is an [example](https://github.com/kubernetes-sigs/controller-runtime/tree/master/examples/builtins) -in controller-runtime. - -It is suggested to use kubebuilder to initialize a project, and then you can -follow the steps below to add admission webhooks for core types. - -## Implementing Your Handler Using `Handle` - -Your handler must implement the [admission.Handler](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#Handler) interface. This function is responsible for both mutating and validating the incoming resource. - -### Update your webhook: - -**Example** - -```go -package v1 - -import ( - "context" - "encoding/json" - "net/http" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - corev1 "k8s.io/api/core/v1" -) - -// **Note**: in order to have controller-gen generate the webhook configuration for you, you need to add markers. For example: - -// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io - -type podAnnotator struct { - Client client.Client - decoder *admission.Decoder -} - -func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response { - pod := &corev1.Pod{} - err := a.decoder.Decode(req, pod) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - // Mutate the fields in pod - pod.Annotations["example.com/mutated"] = "true" - - marshaledPod, err := json.Marshal(pod) - if err != nil { - return admission.Errored(http.StatusInternalServerError, err) - } - return admission.Patched(req.Object.Raw, marshaledPod) -} -``` - - -## Update main.go - -Now you need to register your handler in the webhook server. - -```go -mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{ - Handler: &podAnnotator{Client: mgr.GetClient()}, -}) -``` - -You need to ensure the path here match the path in the marker. - -### Client/Decoder - -If you need a client and/or decoder, just pass them in at struct construction time. - -```go -mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{ - Handler: &podAnnotator{ - Client: mgr.GetClient(), - decoder: admission.NewDecoder(mgr.GetScheme()), - }, -}) -``` - -## By using Custom interfaces instead of Handle - -### Update your webhook: - -**Example** - -```go -package v1 - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - "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" -) - -// log is for logging in this package. -var podlog = logf.Log.WithName("pod-resource") - -// SetupWebhookWithManager will setup the manager to manage the webhooks -func (r *corev1.Pod) SetupWebhookWithManager(mgr ctrl.Manager) error { - runAsNonRoot := true - allowPrivilegeEscalation := false - - return ctrl.NewWebhookManagedBy(mgr). - For(r). - WithValidator(&PodCustomValidator{}). - WithDefaulter(&PodCustomDefaulter{ - DefaultSecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &runAsNonRoot, // Set to true - AllowPrivilegeEscalation: &allowPrivilegeEscalation, // Set to false - }, - }). - Complete() -} - -// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1 - -// +kubebuilder:object:generate=false -// PodCustomDefaulter struct is responsible for setting default values on the Pod resource -// when it is 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 PodCustomDefaulter struct { - // Default security context to be applied to Pods - DefaultSecurityContext *corev1.SecurityContext - - // TODO: Add more fields as needed for defaulting -} - -var _ webhook.CustomDefaulter = &PodCustomDefaulter{} - -// Default implements webhook.CustomDefaulter so a webhook will be registered for the type Pod -func (d *PodCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - pod, ok := obj.(*corev1.Pod) - if !ok { - return fmt.Errorf("expected a Pod object but got %T", obj) - } - podlog.Info("CustomDefaulter for corev1.Pod", "name", pod.GetName()) - - // Apply the default security context if it's not set - for i := range pod.Spec.Containers { - if pod.Spec.Containers[i].SecurityContext == nil { - pod.Spec.Containers[i].SecurityContext = d.DefaultSecurityContext - } - } - - // Mutate the fields in Pod (e.g., adding an annotation) - if pod.Annotations == nil { - pod.Annotations = map[string]string{} - } - pod.Annotations["example.com/mutated"] = "true" - - // TODO: Add any additional defaulting logic here. - - return nil -} - -// +kubebuilder:webhook:path=/validate--v1-pod,mutating=false,failurePolicy=fail,groups="",resources=pods,verbs=create;update;delete,versions=v1,name=vpod.kb.io,admissionReviewVersions=v1 - -// +kubebuilder:object:generate=false -// PodCustomValidator struct is responsible for validating the Pod 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 PodCustomValidator struct { -} - -var _ webhook.CustomValidator = &PodCustomValidator{} - -// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Pod -func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - pod, ok := obj.(*corev1.Pod) - if !ok { - return nil, fmt.Errorf("expected a Pod object but got %T", obj) - } - podlog.Info("Validation for corev1.Pod upon creation", "name", pod.GetName()) - - // Ensure the Pod has at least one container - if len(pod.Spec.Containers) == 0 { - return nil, fmt.Errorf("pod must have at least one container") - } - - // TODO: Add any additional creation validation logic here. - - return nil, nil -} - -// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Pod -func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - pod, ok := newObj.(*corev1.Pod) - if !ok { - return nil, fmt.Errorf("expected a Pod object but got %T", newObj) - } - podlog.Info("Validation for corev1.Pod upon Update", "name", pod.GetName()) - - oldPod := oldObj.(*corev1.Pod) - // Prevent changing a specific annotation - if oldPod.Annotations["example.com/protected"] != pod.Annotations["example.com/protected"] { - return nil, fmt.Errorf("the annotation 'example.com/protected' cannot be changed") - } - - // Prevent changing the security context after creation - for i := range pod.Spec.Containers { - if !equalSecurityContexts(oldPod.Spec.Containers[i].SecurityContext, pod.Spec.Containers[i].SecurityContext) { - return nil, fmt.Errorf("security context of containers cannot be changed after creation") - } - } - - // TODO: Add any additional update validation logic here. - - return nil, nil -} - -// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Pod -func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - pod, ok := obj.(*corev1.Pod) - if !ok { - return nil, fmt.Errorf("expected a Pod object but got %T", obj) - } - podlog.Info("Deletion for corev1.Pod upon Update", "name", pod.GetName()) - - // Prevent deletion of protected Pods - if pod.Annotations["example.com/protected"] == "true" { - return nil, fmt.Errorf("protected pods cannot be deleted") - } - - // TODO: Add any additional deletion validation logic here. - - return nil, nil -} - -// equalSecurityContexts checks if two SecurityContexts are equal -func equalSecurityContexts(a, b *corev1.SecurityContext) bool { - // Implement your logic to compare SecurityContexts here - // For example, you can compare specific fields: - return a.RunAsNonRoot == b.RunAsNonRoot && - a.AllowPrivilegeEscalation == b.AllowPrivilegeEscalation -} - -``` - -### Update the main.go - -```go -if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err := (&corev1.Pod{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "corev1.Pod") - os.Exit(1) - } -} -``` - -## Deploy - -Deploying it is just like deploying a webhook server for CRD. You need to -1) provision the serving certificate -2) deploy the server - -You can follow the [tutorial](/cronjob-tutorial/running.md). - -## What are `Handle` and Custom Interfaces? - -In the context of Kubernetes admission webhooks, the `Handle` function and the custom interfaces (`CustomValidator` and `CustomDefaulter`) are two different approaches to implementing webhook logic. Each serves specific purposes, and the choice between them depends on the needs of your webhook. - -## Purpose of the `Handle` Function - -The `Handle` function is a core part of the admission webhook process. It is responsible for directly processing the incoming admission request and returning an `admission.Response`. This function is particularly useful when you need to handle both validation and mutation within the same function. - -### Mutation - -If your webhook needs to modify the resource (e.g., add or change annotations, labels, or other fields), the `Handle` function is where you would implement this logic. Mutation involves altering the resource before it is persisted in Kubernetes. - -### Response Construction - -The `Handle` function is also responsible for constructing the `admission.Response`, which determines whether the request should be allowed or denied, or if the resource should be patched (mutated). The `Handle` function gives you full control over how the response is built and what changes are applied to the resource. - -## Purpose of Custom Interfaces (`CustomValidator` and `CustomDefaulter`) - -The `CustomValidator` and `CustomDefaulter` interfaces provide a more modular approach to implementing webhook logic. They allow you to separate validation and defaulting (mutation) into distinct methods, making the code easier to maintain and reason about. - -## When to Use Each Approach - -- **Use `Handle` when**: - - You need to both mutate and validate the resource in a single function. - - You want direct control over how the admission response is constructed and returned. - - Your webhook logic is simple and doesn’t require a clear separation of concerns. - -- **Use `CustomValidator` and `CustomDefaulter` when**: - - You want to separate validation and defaulting logic for better modularity. - - Your webhook logic is complex, and separating concerns makes the code easier to manage. - - You don’t need to perform mutation and validation in the same function. - -[cronjob-tutorial]: /cronjob-tutorial/cronjob-tutorial.md \ No newline at end of file diff --git a/pkg/model/resource/resource.go b/pkg/model/resource/resource.go index c455c1f4b57..84000e958f0 100644 --- a/pkg/model/resource/resource.go +++ b/pkg/model/resource/resource.go @@ -45,6 +45,9 @@ type Resource struct { // External specifies if the resource is defined externally. External bool `json:"external,omitempty"` + + // Core specifies if the resource is from Kubernetes API. + Core bool `json:"core,omitempty"` } // Validate checks that the Resource is valid. diff --git a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go index 4e607235ae9..d919b3088e1 100644 --- a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go +++ b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go @@ -86,7 +86,7 @@ func (s *webhookScaffolder) Scaffold() error { &network_policy.NetworkPolicyAllowWebhooks{}, } - if !s.resource.External { + if !s.resource.External && !s.resource.Core { buildScaffold = append(buildScaffold, &crd.Kustomization{}) } diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index e91b57260df..ea55db3eac4 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -131,6 +131,7 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { } else { // Handle core types if domain, found := coreGroups[res.Group]; found { + res.Core = true res.Domain = domain res.Path = path.Join("k8s.io", "api", res.Group, res.Version) } 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 57656166184..a5ac16564bd 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go @@ -158,7 +158,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-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ if .Resource.Core }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ else }}{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ end }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ if .Resource.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 @@ -198,7 +198,7 @@ func (d *{{ .Resource.Kind }}CustomDefaulter) Default(ctx context.Context, obj r // 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-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ if .Resource.Core }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ else }}{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{ end }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ if .Resource.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 bf32916ebb8..b3adca06b6f 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -103,14 +103,6 @@ func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { p.resource = res - // Ensure that if any external API flag is set, both must be provided. - if len(p.options.ExternalAPIPath) != 0 || len(p.options.ExternalAPIDomain) != 0 { - if len(p.options.ExternalAPIPath) == 0 || len(p.options.ExternalAPIDomain) == 0 { - return errors.New("Both '--external-api-path' and '--external-api-domain' must be " + - "specified together when referencing an external API.") - } - } - if len(p.options.ExternalAPIPath) != 0 && len(p.options.ExternalAPIDomain) != 0 && p.isLegacyPath { return errors.New("You cannot scaffold webhooks for external types " + "using the legacy path") @@ -131,7 +123,7 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { resValue, err := p.config.GetResource(p.resource.GVK) res = &resValue if err != nil { - if !p.resource.External { + if !p.resource.External && !p.resource.Core { return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName) } } else if res.Webhooks != nil && !res.Webhooks.IsEmpty() && !p.force { diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 44ab27fdf02..024d2d100cd 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -49,6 +49,8 @@ function scaffold_test_project { $kb create api --group certmanager --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=cert-manager.io # Webhook for External types $kb create webhook --group certmanager --version v1 --kind Issuer --defaulting --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io + # Webhook for Core type + $kb create webhook --group core --version v1 --kind Pod --defaulting fi if [[ $project =~ multigroup ]]; then @@ -76,7 +78,9 @@ function scaffold_test_project { # Controller for External types $kb create api --group certmanager --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=cert-manager.io # Webhook for External types - $kb create webhook --group certmanager --version v1 --kind Issuer --defaulting --programmatic-validation --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io + $kb create webhook --group certmanager --version v1 --kind Issuer --defaulting --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io + # Webhook for Core type + $kb create webhook --group core --version v1 --kind Pod --programmatic-validation fi if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index 64eb25a146d..974caa95814 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -103,6 +103,7 @@ resources: path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1 version: v1 - controller: true + core: true group: apps kind: Deployment path: k8s.io/api/apps/v1 @@ -140,6 +141,13 @@ resources: version: v1 webhooks: defaulting: true + webhookVersion: v1 +- core: true + group: core + kind: Pod + path: k8s.io/api/core/v1 + version: v1 + webhooks: validation: true webhookVersion: v1 - api: diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index 9669f8a182e..4736d7b59a2 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -57,6 +57,7 @@ import ( seacreaturescontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/sea-creatures" shipcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/ship" webhookcertmanagerv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/certmanager/v1" + webhookcorev1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/core/v1" webhookcrewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/crew/v1" webhookexamplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1" webhookshipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/ship/v1" @@ -291,6 +292,13 @@ func main() { os.Exit(1) } } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookcorev1.SetupPodWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Pod") + os.Exit(1) + } + } if err = (&examplecomcontroller.MemcachedReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_core_pods.yaml b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_core_pods.yaml new file mode 100644 index 00000000000..b1ab830f8f6 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_core_pods.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: pods.core diff --git a/testdata/project-v4-multigroup/config/crd/patches/webhook_in_core_pods.yaml b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_core_pods.yaml new file mode 100644 index 00000000000..8fa5d252208 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_core_pods.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pods.core +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/testdata/project-v4-multigroup/config/webhook/manifests.yaml b/testdata/project-v4-multigroup/config/webhook/manifests.yaml index 071566f2ded..a4782259ac4 100644 --- a/testdata/project-v4-multigroup/config/webhook/manifests.yaml +++ b/testdata/project-v4-multigroup/config/webhook/manifests.yaml @@ -76,19 +76,19 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-certmanager-cert-manager-io-v1-issuer + path: /validate--v1-pod failurePolicy: Fail - name: vissuer-v1.kb.io + name: vpod-v1.kb.io rules: - apiGroups: - - certmanager.cert-manager.io + - "" apiVersions: - v1 operations: - CREATE - UPDATE resources: - - issuers + - pods sideEffects: None - admissionReviewVersions: - v1 diff --git a/testdata/project-v4-multigroup/dist/install.yaml b/testdata/project-v4-multigroup/dist/install.yaml index 4e1a3270dd9..7642ce0566e 100644 --- a/testdata/project-v4-multigroup/dist/install.yaml +++ b/testdata/project-v4-multigroup/dist/install.yaml @@ -1886,19 +1886,19 @@ webhooks: service: name: project-v4-multigroup-webhook-service namespace: project-v4-multigroup-system - path: /validate-certmanager-cert-manager-io-v1-issuer + path: /validate--v1-pod failurePolicy: Fail - name: vissuer-v1.kb.io + name: vpod-v1.kb.io rules: - apiGroups: - - certmanager.cert-manager.io + - "" apiVersions: - v1 operations: - CREATE - UPDATE resources: - - issuers + - pods sideEffects: None - admissionReviewVersions: - v1 diff --git a/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go index 984cfff06df..0d0c812333b 100644 --- a/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go @@ -25,7 +25,6 @@ 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" ) // nolint:unused @@ -35,7 +34,6 @@ var issuerlog = logf.Log.WithName("issuer-resource") // SetupIssuerWebhookWithManager registers the webhook for Issuer in the manager. func SetupIssuerWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&certmanagerv1.Issuer{}). - WithValidator(&IssuerCustomValidator{}). WithDefaulter(&IssuerCustomDefaulter{}). Complete() } @@ -68,58 +66,3 @@ func (d *IssuerCustomDefaulter) Default(ctx context.Context, obj runtime.Object) return nil } - -// 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:path=/validate-certmanager-cert-manager-io-v1-issuer,mutating=false,failurePolicy=fail,sideEffects=None,groups=certmanager.cert-manager.io,resources=issuers,verbs=create;update,versions=v1,name=vissuer-v1.kb.io,admissionReviewVersions=v1 - -// IssuerCustomValidator struct is responsible for validating the Issuer 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 IssuerCustomValidator struct { - //TODO(user): Add more fields as needed for validation -} - -var _ webhook.CustomValidator = &IssuerCustomValidator{} - -// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Issuer. -func (v *IssuerCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - issuer, ok := obj.(*certmanagerv1.Issuer) - if !ok { - return nil, fmt.Errorf("expected a Issuer object but got %T", obj) - } - issuerlog.Info("Validation for Issuer upon creation", "name", issuer.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 Issuer. -func (v *IssuerCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - issuer, ok := newObj.(*certmanagerv1.Issuer) - if !ok { - return nil, fmt.Errorf("expected a Issuer object for the newObj but got %T", newObj) - } - issuerlog.Info("Validation for Issuer upon update", "name", issuer.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 Issuer. -func (v *IssuerCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - issuer, ok := obj.(*certmanagerv1.Issuer) - if !ok { - return nil, fmt.Errorf("expected a Issuer object but got %T", obj) - } - issuerlog.Info("Validation for Issuer upon deletion", "name", issuer.GetName()) - - // TODO(user): fill in your validation logic upon object deletion. - - return nil, nil -} diff --git a/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go index c8bf86e7fd0..b9d0dfeeb23 100644 --- a/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go @@ -28,15 +28,12 @@ var _ = Describe("Issuer Webhook", func() { var ( obj *certmanagerv1.Issuer oldObj *certmanagerv1.Issuer - validator IssuerCustomValidator defaulter IssuerCustomDefaulter ) BeforeEach(func() { obj = &certmanagerv1.Issuer{} oldObj = &certmanagerv1.Issuer{} - validator = IssuerCustomValidator{} - Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") defaulter = IssuerCustomDefaulter{} Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") @@ -61,27 +58,4 @@ var _ = Describe("Issuer Webhook", func() { // }) }) - Context("When creating or updating Issuer 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-multigroup/internal/webhook/core/v1/pod_webhook.go b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go new file mode 100644 index 00000000000..125c4afdcfe --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go @@ -0,0 +1,97 @@ +/* +Copyright 2024 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" + + corev1 "k8s.io/api/core/v1" + "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" +) + +// nolint:unused +// log is for logging in this package. +var podlog = logf.Log.WithName("pod-resource") + +// SetupPodWebhookWithManager registers the webhook for Pod in the manager. +func SetupPodWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&corev1.Pod{}). + WithValidator(&PodCustomValidator{}). + Complete() +} + +// 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. +// +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 +// 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 PodCustomValidator struct { + //TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &PodCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Pod. +func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + pod, ok := obj.(*corev1.Pod) + if !ok { + return nil, fmt.Errorf("expected a Pod object but got %T", obj) + } + podlog.Info("Validation for Pod upon creation", "name", pod.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 Pod. +func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + pod, ok := newObj.(*corev1.Pod) + if !ok { + return nil, fmt.Errorf("expected a Pod object for the newObj but got %T", newObj) + } + podlog.Info("Validation for Pod upon update", "name", pod.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 Pod. +func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + pod, ok := obj.(*corev1.Pod) + if !ok { + return nil, fmt.Errorf("expected a Pod object but got %T", obj) + } + podlog.Info("Validation for Pod upon deletion", "name", pod.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook_test.go new file mode 100644 index 00000000000..588a28131e9 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2024 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" + + corev1 "k8s.io/api/core/v1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("Pod Webhook", func() { + var ( + obj *corev1.Pod + oldObj *corev1.Pod + validator PodCustomValidator + ) + + BeforeEach(func() { + obj = &corev1.Pod{} + oldObj = &corev1.Pod{} + validator = PodCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator 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 or updating Pod 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-multigroup/internal/webhook/core/v1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/core/v1/webhook_suite_test.go new file mode 100644 index 00000000000..c6fcf0ebb14 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/core/v1/webhook_suite_test.go @@ -0,0 +1,149 @@ +/* +Copyright 2024 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" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + + // +kubebuilder:scaffold:imports + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = corev1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupPodWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/testdata/project-v4/PROJECT b/testdata/project-v4/PROJECT index df15c87aa21..462e97f303a 100644 --- a/testdata/project-v4/PROJECT +++ b/testdata/project-v4/PROJECT @@ -61,4 +61,12 @@ resources: webhooks: defaulting: true webhookVersion: v1 +- core: true + group: core + kind: Pod + path: k8s.io/api/core/v1 + version: v1 + webhooks: + defaulting: true + webhookVersion: v1 version: "3" diff --git a/testdata/project-v4/cmd/main.go b/testdata/project-v4/cmd/main.go index 763214f0588..da7b88aacd1 100644 --- a/testdata/project-v4/cmd/main.go +++ b/testdata/project-v4/cmd/main.go @@ -40,6 +40,7 @@ import ( crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/controller" webhookcertmanagerv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1" + webhookcorev1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1" webhookcrewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1" // +kubebuilder:scaffold:imports ) @@ -205,6 +206,13 @@ func main() { os.Exit(1) } } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookcorev1.SetupPodWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Pod") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/testdata/project-v4/config/crd/patches/cainjection_in_pods.yaml b/testdata/project-v4/config/crd/patches/cainjection_in_pods.yaml new file mode 100644 index 00000000000..b1ab830f8f6 --- /dev/null +++ b/testdata/project-v4/config/crd/patches/cainjection_in_pods.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: pods.core diff --git a/testdata/project-v4/config/crd/patches/webhook_in_pods.yaml b/testdata/project-v4/config/crd/patches/webhook_in_pods.yaml new file mode 100644 index 00000000000..8fa5d252208 --- /dev/null +++ b/testdata/project-v4/config/crd/patches/webhook_in_pods.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: pods.core +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/testdata/project-v4/config/webhook/manifests.yaml b/testdata/project-v4/config/webhook/manifests.yaml index 17c5114e358..a9842ea8d2c 100644 --- a/testdata/project-v4/config/webhook/manifests.yaml +++ b/testdata/project-v4/config/webhook/manifests.yaml @@ -64,6 +64,26 @@ webhooks: resources: - issuers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate--v1-pod + failurePolicy: Fail + name: mpod-v1.kb.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - pods + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration diff --git a/testdata/project-v4/dist/install.yaml b/testdata/project-v4/dist/install.yaml index e1206818f4d..02efec997fc 100644 --- a/testdata/project-v4/dist/install.yaml +++ b/testdata/project-v4/dist/install.yaml @@ -708,6 +708,26 @@ webhooks: resources: - issuers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: project-v4-webhook-service + namespace: project-v4-system + path: /mutate--v1-pod + failurePolicy: Fail + name: mpod-v1.kb.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - pods + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration diff --git a/testdata/project-v4/internal/webhook/v1/pod_webhook.go b/testdata/project-v4/internal/webhook/v1/pod_webhook.go new file mode 100644 index 00000000000..3671dde49f6 --- /dev/null +++ b/testdata/project-v4/internal/webhook/v1/pod_webhook.go @@ -0,0 +1,68 @@ +/* +Copyright 2024 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" + + corev1 "k8s.io/api/core/v1" + "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" +) + +// nolint:unused +// log is for logging in this package. +var podlog = logf.Log.WithName("pod-resource") + +// SetupPodWebhookWithManager registers the webhook for Pod in the manager. +func SetupPodWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&corev1.Pod{}). + WithDefaulter(&PodCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,sideEffects=None,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod-v1.kb.io,admissionReviewVersions=v1 + +// PodCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Pod 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 PodCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &PodCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Pod. +func (d *PodCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + pod, ok := obj.(*corev1.Pod) + + if !ok { + return fmt.Errorf("expected an Pod object but got %T", obj) + } + podlog.Info("Defaulting for Pod", "name", pod.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} diff --git a/testdata/project-v4/internal/webhook/v1/pod_webhook_test.go b/testdata/project-v4/internal/webhook/v1/pod_webhook_test.go new file mode 100644 index 00000000000..1d7c3191c5a --- /dev/null +++ b/testdata/project-v4/internal/webhook/v1/pod_webhook_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2024 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" + + corev1 "k8s.io/api/core/v1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("Pod Webhook", func() { + var ( + obj *corev1.Pod + oldObj *corev1.Pod + defaulter PodCustomDefaulter + ) + + BeforeEach(func() { + obj = &corev1.Pod{} + oldObj = &corev1.Pod{} + defaulter = PodCustomDefaulter{} + 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 Pod 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")) + // }) + }) + +}) 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 7a5cc39e559..56342e75c66 100644 --- a/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go +++ b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go @@ -127,6 +127,9 @@ var _ = BeforeSuite(func() { err = SetupIssuerWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = SetupPodWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:webhook go func() {