From 1faaf9c4d5bb509ee83e49df94c7d48cbf17783c 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 External Types This update introduces support for scaffolding webhooks for external types, which are APIs/CRDs defined in other projects --- .../reference/using_an_external_resource.md | 14 +- .../common/kustomize/v2/scaffolds/webhook.go | 11 +- pkg/plugins/golang/v4/webhook.go | 32 +++- test/testdata/generate.sh | 4 + testdata/project-v4-multigroup/PROJECT | 10 ++ testdata/project-v4-multigroup/cmd/main.go | 8 + .../cainjection_in_certmanager_issuers.yaml | 7 + .../webhook_in_certmanager_issuers.yaml | 16 ++ .../config/webhook/manifests.yaml | 40 +++++ .../project-v4-multigroup/dist/install.yaml | 40 +++++ .../webhook/certmanager/v1/issuer_webhook.go | 125 +++++++++++++++ .../certmanager/v1/issuer_webhook_test.go | 87 ++++++++++ .../certmanager/v1/webhook_suite_test.go | 149 ++++++++++++++++++ testdata/project-v4/PROJECT | 9 ++ testdata/project-v4/cmd/main.go | 8 + .../crd/patches/cainjection_in_issuers.yaml | 7 + .../crd/patches/webhook_in_issuers.yaml | 16 ++ .../project-v4/config/webhook/manifests.yaml | 20 +++ testdata/project-v4/dist/install.yaml | 20 +++ .../internal/webhook/v1/issuer_webhook.go | 68 ++++++++ .../webhook/v1/issuer_webhook_test.go | 61 +++++++ .../internal/webhook/v1/webhook_suite_test.go | 3 + 22 files changed, 739 insertions(+), 16 deletions(-) create mode 100644 testdata/project-v4-multigroup/config/crd/patches/cainjection_in_certmanager_issuers.yaml create mode 100644 testdata/project-v4-multigroup/config/crd/patches/webhook_in_certmanager_issuers.yaml create mode 100644 testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go create mode 100644 testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go create mode 100644 testdata/project-v4-multigroup/internal/webhook/certmanager/v1/webhook_suite_test.go create mode 100644 testdata/project-v4/config/crd/patches/cainjection_in_issuers.yaml create mode 100644 testdata/project-v4/config/crd/patches/webhook_in_issuers.yaml create mode 100644 testdata/project-v4/internal/webhook/v1/issuer_webhook.go create mode 100644 testdata/project-v4/internal/webhook/v1/issuer_webhook_test.go diff --git a/docs/book/src/reference/using_an_external_resource.md b/docs/book/src/reference/using_an_external_resource.md index 5cecb3609cd..dae069d4a17 100644 --- a/docs/book/src/reference/using_an_external_resource.md +++ b/docs/book/src/reference/using_an_external_resource.md @@ -75,15 +75,11 @@ definitions since the type is defined in an external project. ### Creating a Webhook to Manage an External Type - +```shell +kubebuilder 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 +``` ## Managing Core Types @@ -169,8 +165,6 @@ Also, the RBAC for the above markers: - update ``` -``` - This scaffolds a controller for the Core type `corev1.Pod` but skips creating new resource definitions since the type is already defined in the Kubernetes API. diff --git a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go index d6d8328eb25..dc32b04ebda 100644 --- a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go +++ b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go @@ -73,7 +73,7 @@ func (s *webhookScaffolder) Scaffold() error { return fmt.Errorf("error updating resource: %w", err) } - if err := scaffold.Execute( + buildScaffold := []machinery.Builder{ &kdefault.WebhookCAInjectionPatch{}, &kdefault.ManagerWebhookPatch{}, &webhook.Kustomization{Force: s.force}, @@ -85,8 +85,13 @@ func (s *webhookScaffolder) Scaffold() error { &patches.EnableWebhookPatch{}, &patches.EnableCAInjectionPatch{}, &network_policy.NetworkPolicyAllowWebhooks{}, - &crd.Kustomization{}, - ); err != nil { + } + + if !s.resource.External { + buildScaffold = append(buildScaffold, &crd.Kustomization{}) + } + + if err := scaffold.Execute(buildScaffold...); err != nil { return fmt.Errorf("error scaffolding kustomize webhook manifests: %v", err) } diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go index a78ddff850e..bf32916ebb8 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -17,6 +17,7 @@ limitations under the License. package v4 import ( + "errors" "fmt" "github.com/spf13/pflag" @@ -82,6 +83,14 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+ "This option will be removed in future versions.") + fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "", + "Specify the Go package import path for the external API. This is used to scaffold controllers for resources "+ + "defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).") + + fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "", + "Specify the domain name for the external API. This domain is used to generate accurate RBAC "+ + "markers and permissions for the external resources (e.g., cert-manager.io).") + fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") } @@ -94,6 +103,19 @@ 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") + } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { @@ -106,9 +128,13 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { } // check if resource exist to create webhook - if r, err := p.config.GetResource(p.resource.GVK); err != nil { - return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName) - } else if r.Webhooks != nil && !r.Webhooks.IsEmpty() && !p.force { + resValue, err := p.config.GetResource(p.resource.GVK) + res = &resValue + if err != nil { + if !p.resource.External { + return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName) + } + } else if res.Webhooks != nil && !res.Webhooks.IsEmpty() && !p.force { return fmt.Errorf("webhook resource already exists") } diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index b9888de408d..44ab27fdf02 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -47,6 +47,8 @@ function scaffold_test_project { $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting # 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 --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io fi if [[ $project =~ multigroup ]]; then @@ -73,6 +75,8 @@ function scaffold_test_project { $kb create api --group fiz --version v1 --kind Bar --controller=true --resource=true --make=false # 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 fi if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index 8d854bab7a5..64eb25a146d 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -132,6 +132,16 @@ resources: kind: Certificate path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 version: v1 +- domain: cert-manager.io + external: true + group: certmanager + kind: Issuer + path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 + version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index e57587069b1..9669f8a182e 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -56,6 +56,7 @@ import ( foopolicycontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/foo.policy" 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" 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" @@ -283,6 +284,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Certificate") os.Exit(1) } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookcertmanagerv1.SetupIssuerWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Issuer") + 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_certmanager_issuers.yaml b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_certmanager_issuers.yaml new file mode 100644 index 00000000000..9a2e6b35dc5 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/cainjection_in_certmanager_issuers.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: issuers.certmanager.cert-manager.io diff --git a/testdata/project-v4-multigroup/config/crd/patches/webhook_in_certmanager_issuers.yaml b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_certmanager_issuers.yaml new file mode 100644 index 00000000000..4a738119c1e --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/patches/webhook_in_certmanager_issuers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: issuers.certmanager.cert-manager.io +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 3f6221647a1..071566f2ded 100644 --- a/testdata/project-v4-multigroup/config/webhook/manifests.yaml +++ b/testdata/project-v4-multigroup/config/webhook/manifests.yaml @@ -4,6 +4,26 @@ kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: missuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -50,6 +70,26 @@ kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: vissuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/testdata/project-v4-multigroup/dist/install.yaml b/testdata/project-v4-multigroup/dist/install.yaml index d755f2547d2..b6312506c8d 100644 --- a/testdata/project-v4-multigroup/dist/install.yaml +++ b/testdata/project-v4-multigroup/dist/install.yaml @@ -1814,6 +1814,26 @@ kind: MutatingWebhookConfiguration metadata: name: project-v4-multigroup-mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: project-v4-multigroup-webhook-service + namespace: project-v4-multigroup-system + path: /mutate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: missuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -1860,6 +1880,26 @@ kind: ValidatingWebhookConfiguration metadata: name: project-v4-multigroup-validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: project-v4-multigroup-webhook-service + namespace: project-v4-multigroup-system + path: /validate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: vissuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: 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 new file mode 100644 index 00000000000..984cfff06df --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook.go @@ -0,0 +1,125 @@ +/* +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" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/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 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() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-certmanager-cert-manager-io-v1-issuer,mutating=true,failurePolicy=fail,sideEffects=None,groups=certmanager.cert-manager.io,resources=issuers,verbs=create;update,versions=v1,name=missuer-v1.kb.io,admissionReviewVersions=v1 + +// IssuerCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Issuer 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 IssuerCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &IssuerCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Issuer. +func (d *IssuerCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + issuer, ok := obj.(*certmanagerv1.Issuer) + + if !ok { + return fmt.Errorf("expected an Issuer object but got %T", obj) + } + issuerlog.Info("Defaulting for Issuer", "name", issuer.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: 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 new file mode 100644 index 00000000000..c8bf86e7fd0 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/issuer_webhook_test.go @@ -0,0 +1,87 @@ +/* +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" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + // TODO (user): Add any additional imports if needed +) + +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") + 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 Issuer 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 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/certmanager/v1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/certmanager/v1/webhook_suite_test.go new file mode 100644 index 00000000000..cd99fe28462 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/certmanager/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" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + admissionv1 "k8s.io/api/admission/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 = certmanagerv1.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 = SetupIssuerWebhookWithManager(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 e65c3db0df4..df15c87aa21 100644 --- a/testdata/project-v4/PROJECT +++ b/testdata/project-v4/PROJECT @@ -52,4 +52,13 @@ resources: kind: Certificate path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 version: v1 +- domain: cert-manager.io + external: true + group: certmanager + kind: Issuer + path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/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 b3384d0d988..763214f0588 100644 --- a/testdata/project-v4/cmd/main.go +++ b/testdata/project-v4/cmd/main.go @@ -39,6 +39,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" webhookcrewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1" // +kubebuilder:scaffold:imports ) @@ -197,6 +198,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Certificate") os.Exit(1) } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookcertmanagerv1.SetupIssuerWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Issuer") + 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_issuers.yaml b/testdata/project-v4/config/crd/patches/cainjection_in_issuers.yaml new file mode 100644 index 00000000000..9a2e6b35dc5 --- /dev/null +++ b/testdata/project-v4/config/crd/patches/cainjection_in_issuers.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: issuers.certmanager.cert-manager.io diff --git a/testdata/project-v4/config/crd/patches/webhook_in_issuers.yaml b/testdata/project-v4/config/crd/patches/webhook_in_issuers.yaml new file mode 100644 index 00000000000..4a738119c1e --- /dev/null +++ b/testdata/project-v4/config/crd/patches/webhook_in_issuers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: issuers.certmanager.cert-manager.io +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 002aef077f4..17c5114e358 100644 --- a/testdata/project-v4/config/webhook/manifests.yaml +++ b/testdata/project-v4/config/webhook/manifests.yaml @@ -44,6 +44,26 @@ webhooks: resources: - captains sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: missuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + 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 bc03981dc80..8197115b32c 100644 --- a/testdata/project-v4/dist/install.yaml +++ b/testdata/project-v4/dist/install.yaml @@ -688,6 +688,26 @@ webhooks: resources: - captains sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: project-v4-webhook-service + namespace: project-v4-system + path: /mutate-certmanager-cert-manager-io-v1-issuer + failurePolicy: Fail + name: missuer-v1.kb.io + rules: + - apiGroups: + - certmanager.cert-manager.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - issuers + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration diff --git a/testdata/project-v4/internal/webhook/v1/issuer_webhook.go b/testdata/project-v4/internal/webhook/v1/issuer_webhook.go new file mode 100644 index 00000000000..0d0c812333b --- /dev/null +++ b/testdata/project-v4/internal/webhook/v1/issuer_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" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/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 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{}). + WithDefaulter(&IssuerCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-certmanager-cert-manager-io-v1-issuer,mutating=true,failurePolicy=fail,sideEffects=None,groups=certmanager.cert-manager.io,resources=issuers,verbs=create;update,versions=v1,name=missuer-v1.kb.io,admissionReviewVersions=v1 + +// IssuerCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Issuer 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 IssuerCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &IssuerCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Issuer. +func (d *IssuerCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + issuer, ok := obj.(*certmanagerv1.Issuer) + + if !ok { + return fmt.Errorf("expected an Issuer object but got %T", obj) + } + issuerlog.Info("Defaulting for Issuer", "name", issuer.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} diff --git a/testdata/project-v4/internal/webhook/v1/issuer_webhook_test.go b/testdata/project-v4/internal/webhook/v1/issuer_webhook_test.go new file mode 100644 index 00000000000..b9d0dfeeb23 --- /dev/null +++ b/testdata/project-v4/internal/webhook/v1/issuer_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" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("Issuer Webhook", func() { + var ( + obj *certmanagerv1.Issuer + oldObj *certmanagerv1.Issuer + defaulter IssuerCustomDefaulter + ) + + BeforeEach(func() { + obj = &certmanagerv1.Issuer{} + oldObj = &certmanagerv1.Issuer{} + defaulter = IssuerCustomDefaulter{} + 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 Issuer 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 c49251cd192..7a5cc39e559 100644 --- a/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go +++ b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go @@ -124,6 +124,9 @@ var _ = BeforeSuite(func() { err = SetupAdmiralWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = SetupIssuerWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:webhook go func() {