From 5e42196369680f35478aaac8b56ddde05544a7ea Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Tue, 10 Sep 2024 19:36:37 +0100 Subject: [PATCH] :warning: (go/v4) decouple webhooks from APIs - Move Webhooks from `api/` or `api//` to `internal/webhook/` or `internal/webhook//` This PR decouples the webhooks from the API, aligning with the recent breaking changes introduced in controller-runtime to ensure that kubebuilder still compatbile with its next release. Webhooks are now scaffolded under `internal/webhook` to comply with the latest standards. **Context:** Controller-runtime deprecated and removed the webhook methods in favor of CustomInterfaces (see [controller-runtime#2641](https://github.com/kubernetes-sigs/controller-runtime/issues/2641)). The motivation for this change is outlined in [controller-runtime#2596](https://github.com/kubernetes-sigs/controller-runtime/issues/2596). See that the current master branch already reflects these changes, using the CustomInterfaces: [kubebuilder#4060](https://github.com/kubernetes-sigs/kubebuilder/pull/4060). **Changes:** - Webhooks are now scaffolded in `internal/webhook/` or `internal/webhook//`. - However, to ensure backwards compatibility, a new `--legacy` flag is introduced. Running `kubebuilder create webhook [options] --legacy` will scaffold webhooks in the legacy location for projects that need to retain the old structure. However, users will still to address the breaking changes in the source code by replacing the old methods by the new CustomInterfaces. --- .../testdata/project/Dockerfile | 2 +- .../project/api/v1/webhook_suite_test.go | 147 ----------------- .../project/api/v1/zz_generated.deepcopy.go | 2 +- .../testdata/project/cmd/main.go | 3 +- .../webhook}/v1/cronjob_webhook.go | 74 +++++---- .../webhook}/v1/cronjob_webhook_test.go | 35 ++-- .../webhook}/v1/webhook_suite_test.go | 7 +- .../webhook-implementation.md | 2 +- .../testdata/project/Dockerfile | 2 +- .../src/multiversion-tutorial/conversion.md | 4 +- .../testdata/project/Dockerfile | 2 +- .../project/api/v1/webhook_suite_test.go | 147 ----------------- .../project/api/v1/zz_generated.deepcopy.go | 2 +- .../project/api/v2/webhook_suite_test.go | 147 ----------------- .../project/api/v2/zz_generated.deepcopy.go | 2 +- .../testdata/project/cmd/main.go | 6 +- .../webhook}/v1/cronjob_conversion.go | 15 +- .../webhook}/v1/cronjob_webhook.go | 74 +++++---- .../webhook}/v1/cronjob_webhook_test.go | 35 ++-- .../webhook}/v1/webhook_suite_test.go | 7 +- .../webhook}/v2/cronjob_conversion.go | 37 +++-- .../webhook}/v2/cronjob_webhook.go | 102 ++++++------ .../webhook}/v2/cronjob_webhook_test.go | 10 +- .../webhook/v2}/webhook_suite_test.go | 9 +- .../src/multiversion-tutorial/webhooks.md | 2 +- go.mod | 6 +- go.sum | 18 ++- .../cronjob-tutorial/generate_cronjob.go | 57 +++---- .../webhook_implementation.go | 82 +++++----- .../generate_multiversion.go | 81 +++------- .../internal/multiversion-tutorial/hub.go | 58 +++++-- .../webhook_v2_implementaton.go | 102 +++++++----- .../internal/templates/dockerfile.go | 2 +- .../v4/scaffolds/internal/templates/main.go | 39 ++++- .../templates/{api => webhooks}/webhook.go | 66 +++++++- .../{api => webhooks}/webhook_suitetest.go | 63 ++++++-- .../webhook_test_template.go | 46 +++++- pkg/plugins/golang/v4/scaffolds/webhook.go | 32 +++- pkg/plugins/golang/v4/webhook.go | 11 +- test/e2e/v4/generate_test.go | 6 +- testdata/project-v4-multigroup/Dockerfile | 2 +- .../api/crew/v1/zz_generated.deepcopy.go | 2 +- .../v1alpha1/zz_generated.deepcopy.go | 2 +- .../api/ship/v1/zz_generated.deepcopy.go | 2 +- .../ship/v2alpha1/zz_generated.deepcopy.go | 2 +- testdata/project-v4-multigroup/cmd/main.go | 15 +- .../webhook}/crew/v1/captain_webhook.go | 20 +-- .../webhook/crew}/v1/captain_webhook_test.go | 8 +- .../webhook/crew/v1/webhook_suite_test.go | 150 +++++++++++++++++ .../v1alpha1/memcached_webhook.go | 17 +- .../v1alpha1/memcached_webhook_test.go | 8 +- .../v1alpha1/webhook_suite_test.go | 150 +++++++++++++++++ .../webhook}/ship/v1/destroyer_webhook.go | 12 +- .../ship/v1/destroyer_webhook_test.go | 6 +- .../webhook/ship/v1/webhook_suite_test.go | 150 +++++++++++++++++ .../webhook}/ship/v1beta1/frigate_webhook.go | 9 +- .../ship/v1beta1/frigate_webhook_test.go | 8 +- .../webhook}/ship/v2alpha1/cruiser_webhook.go | 17 +- .../ship/v2alpha1/cruiser_webhook_test.go | 8 +- .../ship/v2alpha1/webhook_suite_test.go | 150 +++++++++++++++++ testdata/project-v4-with-plugins/Dockerfile | 2 +- .../api/v1alpha1/webhook_suite_test.go | 147 ----------------- .../api/v1alpha1/zz_generated.deepcopy.go | 2 +- testdata/project-v4-with-plugins/cmd/main.go | 3 +- .../webhook}/v1alpha1/memcached_webhook.go | 17 +- .../v1alpha1/memcached_webhook_test.go | 8 +- .../webhook}/v1alpha1/webhook_suite_test.go | 7 +- testdata/project-v4/Dockerfile | 2 +- .../project-v4/api/v1/webhook_suite_test.go | 150 ----------------- .../api/v1/zz_generated.deepcopy.go | 2 +- testdata/project-v4/cmd/main.go | 7 +- .../webhook}/v1/admiral_webhook.go | 12 +- .../webhook}/v1/admiral_webhook_test.go | 6 +- .../webhook}/v1/captain_webhook.go | 20 +-- .../webhook}/v1/captain_webhook_test.go | 8 +- .../webhook}/v1/firstmate_webhook.go | 9 +- .../webhook}/v1/firstmate_webhook_test.go | 8 +- .../internal/webhook/v1/webhook_suite_test.go | 153 ++++++++++++++++++ 78 files changed, 1593 insertions(+), 1252 deletions(-) delete mode 100644 docs/book/src/cronjob-tutorial/testdata/project/api/v1/webhook_suite_test.go rename docs/book/src/cronjob-tutorial/testdata/project/{api => internal/webhook}/v1/cronjob_webhook.go (81%) rename docs/book/src/cronjob-tutorial/testdata/project/{api => internal/webhook}/v1/cronjob_webhook_test.go (84%) rename {testdata/project-v4-multigroup/api/crew => docs/book/src/cronjob-tutorial/testdata/project/internal/webhook}/v1/webhook_suite_test.go (97%) delete mode 100644 docs/book/src/multiversion-tutorial/testdata/project/api/v1/webhook_suite_test.go delete mode 100644 docs/book/src/multiversion-tutorial/testdata/project/api/v2/webhook_suite_test.go rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v1/cronjob_conversion.go (70%) rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v1/cronjob_webhook.go (81%) rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v1/cronjob_webhook_test.go (84%) rename {testdata/project-v4-multigroup/api/ship => docs/book/src/multiversion-tutorial/testdata/project/internal/webhook}/v1/webhook_suite_test.go (97%) rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v2/cronjob_conversion.go (73%) rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v2/cronjob_webhook.go (69%) rename docs/book/src/multiversion-tutorial/testdata/project/{api => internal/webhook}/v2/cronjob_webhook_test.go (92%) rename {testdata/project-v4-multigroup/api/ship/v2alpha1 => docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2}/webhook_suite_test.go (96%) rename pkg/plugins/golang/v4/scaffolds/internal/templates/{api => webhooks}/webhook.go (77%) rename pkg/plugins/golang/v4/scaffolds/internal/templates/{api => webhooks}/webhook_suitetest.go (76%) rename pkg/plugins/golang/v4/scaffolds/internal/templates/{api => webhooks}/webhook_test_template.go (72%) rename testdata/project-v4-multigroup/{api => internal/webhook}/crew/v1/captain_webhook.go (90%) rename testdata/{project-v4/api => project-v4-multigroup/internal/webhook/crew}/v1/captain_webhook_test.go (92%) create mode 100644 testdata/project-v4-multigroup/internal/webhook/crew/v1/webhook_suite_test.go rename testdata/{project-v4-with-plugins/api => project-v4-multigroup/internal/webhook/example.com}/v1alpha1/memcached_webhook.go (86%) rename testdata/project-v4-multigroup/{api => internal/webhook}/example.com/v1alpha1/memcached_webhook_test.go (87%) create mode 100644 testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/webhook_suite_test.go rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v1/destroyer_webhook.go (87%) rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v1/destroyer_webhook_test.go (91%) create mode 100644 testdata/project-v4-multigroup/internal/webhook/ship/v1/webhook_suite_test.go rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v1beta1/frigate_webhook.go (74%) rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v1beta1/frigate_webhook_test.go (87%) rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v2alpha1/cruiser_webhook.go (87%) rename testdata/project-v4-multigroup/{api => internal/webhook}/ship/v2alpha1/cruiser_webhook_test.go (89%) create mode 100644 testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/webhook_suite_test.go delete mode 100644 testdata/project-v4-with-plugins/api/v1alpha1/webhook_suite_test.go rename testdata/{project-v4-multigroup/api/example.com => project-v4-with-plugins/internal/webhook}/v1alpha1/memcached_webhook.go (86%) rename testdata/project-v4-with-plugins/{api => internal/webhook}/v1alpha1/memcached_webhook_test.go (88%) rename testdata/{project-v4-multigroup/api/example.com => project-v4-with-plugins/internal/webhook}/v1alpha1/webhook_suite_test.go (95%) delete mode 100644 testdata/project-v4/api/v1/webhook_suite_test.go rename testdata/project-v4/{api => internal/webhook}/v1/admiral_webhook.go (87%) rename testdata/project-v4/{api => internal/webhook}/v1/admiral_webhook_test.go (92%) rename testdata/project-v4/{api => internal/webhook}/v1/captain_webhook.go (90%) rename testdata/{project-v4-multigroup/api/crew => project-v4/internal/webhook}/v1/captain_webhook_test.go (92%) rename testdata/project-v4/{api => internal/webhook}/v1/firstmate_webhook.go (76%) rename testdata/project-v4/{api => internal/webhook}/v1/firstmate_webhook_test.go (89%) create mode 100644 testdata/project-v4/internal/webhook/v1/webhook_suite_test.go diff --git a/docs/book/src/cronjob-tutorial/testdata/project/Dockerfile b/docs/book/src/cronjob-tutorial/testdata/project/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/Dockerfile +++ b/docs/book/src/cronjob-tutorial/testdata/project/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/webhook_suite_test.go b/docs/book/src/cronjob-tutorial/testdata/project/api/v1/webhook_suite_test.go deleted file mode 100644 index e10bdd75482..00000000000 --- a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/webhook_suite_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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" - // +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 = 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 = (&CronJob{}).SetupWebhookWithManager(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/docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go b/docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go index 30f97db255f..5f32c3ab478 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go b/docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go index ab7399bf0c4..cc1f7055a59 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go @@ -38,6 +38,7 @@ import ( batchv1 "tutorial.kubebuilder.io/project/api/v1" "tutorial.kubebuilder.io/project/internal/controller" + webhookbatchv1 "tutorial.kubebuilder.io/project/internal/webhook/v1" // +kubebuilder:scaffold:imports ) @@ -183,7 +184,7 @@ func main() { */ // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&batchv1.CronJob{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookbatchv1.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CronJob") os.Exit(1) } diff --git a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook.go b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go similarity index 81% rename from docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook.go rename to docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go index db59768a51a..f9829629314 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go @@ -31,6 +31,8 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Go imports @@ -45,13 +47,12 @@ var cronjoblog = logf.Log.WithName("cronjob-resource") Then, we set up the webhook with the manager. */ -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv1.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ - DefaultConcurrencyPolicy: AllowConcurrent, + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, @@ -81,7 +82,7 @@ This marker is responsible for generating a mutation webhook manifest. type CronJobCustomDefaulter struct { // Default values for various CronJob fields - DefaultConcurrencyPolicy ConcurrencyPolicy + DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 @@ -98,32 +99,34 @@ The `Default`method is expected to mutate the receiver, setting the defaults. // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) + if !ok { return fmt.Errorf("expected an CronJob object but got %T", obj) } cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName()) // Set default values - cronjob.Default() - + d.applyDefaults(cronjob) return nil } -func (r *CronJob) Default() { - if r.Spec.ConcurrencyPolicy == "" { - r.Spec.ConcurrencyPolicy = AllowConcurrent +// applyDefaults applies default values to CronJob fields. +func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { + if cronJob.Spec.ConcurrencyPolicy == "" { + cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } - if r.Spec.Suspend == nil { - r.Spec.Suspend = new(bool) + if cronJob.Spec.Suspend == nil { + cronJob.Spec.Suspend = new(bool) + *cronJob.Spec.Suspend = d.DefaultSuspend } - if r.Spec.SuccessfulJobsHistoryLimit == nil { - r.Spec.SuccessfulJobsHistoryLimit = new(int32) - *r.Spec.SuccessfulJobsHistoryLimit = 3 + if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) + *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } - if r.Spec.FailedJobsHistoryLimit == nil { - r.Spec.FailedJobsHistoryLimit = new(int32) - *r.Spec.FailedJobsHistoryLimit = 1 + if cronJob.Spec.FailedJobsHistoryLimit == nil { + cronJob.Spec.FailedJobsHistoryLimit = new(int32) + *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } @@ -168,29 +171,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv1.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -205,12 +208,13 @@ func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime We validate the name and the spec of the CronJob. */ -func (r *CronJob) validateCronJob() error { +// validateCronJob validates the fields of a CronJob object. +func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList - if err := r.validateCronJobName(); err != nil { + if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } - if err := r.validateCronJobSpec(); err != nil { + if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { @@ -219,7 +223,7 @@ func (r *CronJob) validateCronJob() error { return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, - r.Name, allErrs) + cronjob.Name, allErrs) } /* @@ -232,11 +236,11 @@ declaring validation by running `controller-gen crd -w`, or [here](/reference/markers/crd-validation.md). */ -func (r *CronJob) validateCronJobSpec() *field.Error { +func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( - r.Spec.Schedule, + cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } @@ -261,15 +265,15 @@ the apimachinery repo, so we can't declaratively validate it using the validation schema. */ -func (r *CronJob) validateCronJobName() *field.Error { - if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { +func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { + if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. - return field.Invalid(field.NewPath("metadata").Child("name"), r.ObjectMeta.Name, "must be no more than 52 characters") + return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters") } return nil } diff --git a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook_test.go b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go similarity index 84% rename from docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook_test.go rename to docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go index bb82eb2cc3d..0f8bfd0ddee 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook_test.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go @@ -19,21 +19,24 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("CronJob Webhook", func() { var ( - obj *CronJob - oldObj *CronJob + obj = &batchv1.CronJob{} + oldObj = &batchv1.CronJob{} validator CronJobCustomValidator + defaulter CronJobCustomDefaulter ) BeforeEach(func() { - obj = &CronJob{ - Spec: CronJobSpec{ + obj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -41,10 +44,10 @@ var _ = Describe("CronJob Webhook", func() { *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 - oldObj = &CronJob{ - Spec: CronJobSpec{ + oldObj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -53,6 +56,12 @@ var _ = Describe("CronJob Webhook", func() { *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} + defaulter = CronJobCustomDefaulter{ + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, + DefaultSuspend: false, + DefaultSuccessfulJobsHistoryLimit: 3, + DefaultFailedJobsHistoryLimit: 1, + } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") @@ -71,10 +80,10 @@ var _ = Describe("CronJob Webhook", func() { obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the default values are set") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") @@ -82,7 +91,7 @@ var _ = Describe("CronJob Webhook", func() { It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") - obj.Spec.ConcurrencyPolicy = ForbidConcurrent + obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = new(bool) *obj.Spec.Suspend = true obj.Spec.SuccessfulJobsHistoryLimit = new(int32) @@ -91,10 +100,10 @@ var _ = Describe("CronJob Webhook", func() { *obj.Spec.FailedJobsHistoryLimit = 2 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") diff --git a/testdata/project-v4-multigroup/api/crew/v1/webhook_suite_test.go b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go similarity index 97% rename from testdata/project-v4-multigroup/api/crew/v1/webhook_suite_test.go rename to docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go index 6614182b4e2..1b47dd5c702 100644 --- a/testdata/project-v4-multigroup/api/crew/v1/webhook_suite_test.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go @@ -30,6 +30,9 @@ import ( . "github.com/onsi/gomega" admissionv1 "k8s.io/api/admission/v1" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" + // +kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" @@ -89,7 +92,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() - err = AddToScheme(scheme) + err = batchv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1.AddToScheme(scheme) @@ -115,7 +118,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Captain{}).SetupWebhookWithManager(mgr) + err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook diff --git a/docs/book/src/cronjob-tutorial/webhook-implementation.md b/docs/book/src/cronjob-tutorial/webhook-implementation.md index 88215eb537e..423f27f73b1 100644 --- a/docs/book/src/cronjob-tutorial/webhook-implementation.md +++ b/docs/book/src/cronjob-tutorial/webhook-implementation.md @@ -19,4 +19,4 @@ kubebuilder create webhook --group batch --version v1 --kind CronJob --defaultin This will scaffold the webhook functions and register your webhook with the manager in your `main.go` for you. -{{#literatego ./testdata/project/api/v1/cronjob_webhook.go}} +{{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}} diff --git a/docs/book/src/getting-started/testdata/project/Dockerfile b/docs/book/src/getting-started/testdata/project/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/docs/book/src/getting-started/testdata/project/Dockerfile +++ b/docs/book/src/getting-started/testdata/project/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/docs/book/src/multiversion-tutorial/conversion.md b/docs/book/src/multiversion-tutorial/conversion.md index 443b2a79284..3127d7cfa56 100644 --- a/docs/book/src/multiversion-tutorial/conversion.md +++ b/docs/book/src/multiversion-tutorial/conversion.md @@ -9,13 +9,13 @@ cluttering up our main types file with extra functions. First, we'll implement the hub. We'll choose the v1 version as the hub: -{{#literatego ./testdata/project/api/v1/cronjob_conversion.go}} +{{#literatego ./testdata/project/internal/webhook/v1/cronjob_conversion.go}} ## ... and Spokes Then, we'll implement our spoke, the v2 version: -{{#literatego ./testdata/project/api/v2/cronjob_conversion.go}} +{{#literatego ./testdata/project/internal/webhook/v2/cronjob_conversion.go}} Now that we've got our conversions in place, all that we need to do is wire up our main to serve the webhook! diff --git a/docs/book/src/multiversion-tutorial/testdata/project/Dockerfile b/docs/book/src/multiversion-tutorial/testdata/project/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/Dockerfile +++ b/docs/book/src/multiversion-tutorial/testdata/project/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/webhook_suite_test.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/webhook_suite_test.go deleted file mode 100644 index e10bdd75482..00000000000 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/webhook_suite_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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" - // +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 = 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 = (&CronJob{}).SetupWebhookWithManager(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/docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go index 30f97db255f..5f32c3ab478 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/webhook_suite_test.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v2/webhook_suite_test.go deleted file mode 100644 index 9d8ad182e4d..00000000000 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/webhook_suite_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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 v2 - -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" - // +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 = 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 = (&CronJob{}).SetupWebhookWithManager(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/docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go index 384a9df866c..5ea5cddb2d2 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v2 import ( "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/docs/book/src/multiversion-tutorial/testdata/project/cmd/main.go b/docs/book/src/multiversion-tutorial/testdata/project/cmd/main.go index 65f0a6405bc..1685ad14110 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/cmd/main.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/cmd/main.go @@ -40,6 +40,8 @@ import ( batchv1 "tutorial.kubebuilder.io/project/api/v1" batchv2 "tutorial.kubebuilder.io/project/api/v2" "tutorial.kubebuilder.io/project/internal/controller" + webhookbatchv1 "tutorial.kubebuilder.io/project/internal/webhook/v1" + webhookbatchv2 "tutorial.kubebuilder.io/project/internal/webhook/v2" // +kubebuilder:scaffold:imports ) @@ -175,14 +177,14 @@ func main() { */ // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&batchv1.CronJob{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookbatchv1.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CronJob") os.Exit(1) } } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&batchv2.CronJob{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookbatchv2.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CronJob") os.Exit(1) } diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_conversion.go similarity index 70% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_conversion.go index 36485072ec8..6d7d8cab598 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_conversion.go @@ -22,5 +22,18 @@ method called `Hub()` to serve as a We could also just put this inline in our `cronjob_types.go` file. */ +import ( + batchv1 "tutorial.kubebuilder.io/project/api/v1" // Import CronJob from API package +) + +/* +ConversionCronJob wraps the API's CronJob and implements the Hub interface for conversion. +*/ + +// ConversionCronJob wraps the existing CronJob type from the API. +type ConversionCronJob struct { + batchv1.CronJob // Embed the original CronJob type +} + // Hub marks this type as a conversion hub. -func (*CronJob) Hub() {} +func (*ConversionCronJob) Hub() {} diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go similarity index 81% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go index 98bdafe18c8..eecaefb2705 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go @@ -31,6 +31,8 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Go imports @@ -49,13 +51,12 @@ types implement the interfaces, a conversion webhook will be registered. */ -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv1.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ - DefaultConcurrencyPolicy: AllowConcurrent, + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, @@ -85,7 +86,7 @@ This marker is responsible for generating a mutation webhook manifest. type CronJobCustomDefaulter struct { // Default values for various CronJob fields - DefaultConcurrencyPolicy ConcurrencyPolicy + DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 @@ -102,32 +103,34 @@ The `Default`method is expected to mutate the receiver, setting the defaults. // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) + if !ok { return fmt.Errorf("expected an CronJob object but got %T", obj) } cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName()) // Set default values - cronjob.Default() - + d.applyDefaults(cronjob) return nil } -func (r *CronJob) Default() { - if r.Spec.ConcurrencyPolicy == "" { - r.Spec.ConcurrencyPolicy = AllowConcurrent +// applyDefaults applies default values to CronJob fields. +func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { + if cronJob.Spec.ConcurrencyPolicy == "" { + cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } - if r.Spec.Suspend == nil { - r.Spec.Suspend = new(bool) + if cronJob.Spec.Suspend == nil { + cronJob.Spec.Suspend = new(bool) + *cronJob.Spec.Suspend = d.DefaultSuspend } - if r.Spec.SuccessfulJobsHistoryLimit == nil { - r.Spec.SuccessfulJobsHistoryLimit = new(int32) - *r.Spec.SuccessfulJobsHistoryLimit = 3 + if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) + *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } - if r.Spec.FailedJobsHistoryLimit == nil { - r.Spec.FailedJobsHistoryLimit = new(int32) - *r.Spec.FailedJobsHistoryLimit = 1 + if cronJob.Spec.FailedJobsHistoryLimit == nil { + cronJob.Spec.FailedJobsHistoryLimit = new(int32) + *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } @@ -172,29 +175,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv1.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -209,12 +212,13 @@ func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime We validate the name and the spec of the CronJob. */ -func (r *CronJob) validateCronJob() error { +// validateCronJob validates the fields of a CronJob object. +func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList - if err := r.validateCronJobName(); err != nil { + if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } - if err := r.validateCronJobSpec(); err != nil { + if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { @@ -223,7 +227,7 @@ func (r *CronJob) validateCronJob() error { return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, - r.Name, allErrs) + cronjob.Name, allErrs) } /* @@ -236,11 +240,11 @@ declaring validation by running `controller-gen crd -w`, or [here](/reference/markers/crd-validation.md). */ -func (r *CronJob) validateCronJobSpec() *field.Error { +func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( - r.Spec.Schedule, + cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } @@ -265,15 +269,15 @@ the apimachinery repo, so we can't declaratively validate it using the validation schema. */ -func (r *CronJob) validateCronJobName() *field.Error { - if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { +func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { + if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. - return field.Invalid(field.NewPath("metadata").Child("name"), r.ObjectMeta.Name, "must be no more than 52 characters") + return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters") } return nil } diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go similarity index 84% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook_test.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go index bb82eb2cc3d..0f8bfd0ddee 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_webhook_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go @@ -19,21 +19,24 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("CronJob Webhook", func() { var ( - obj *CronJob - oldObj *CronJob + obj = &batchv1.CronJob{} + oldObj = &batchv1.CronJob{} validator CronJobCustomValidator + defaulter CronJobCustomDefaulter ) BeforeEach(func() { - obj = &CronJob{ - Spec: CronJobSpec{ + obj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -41,10 +44,10 @@ var _ = Describe("CronJob Webhook", func() { *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 - oldObj = &CronJob{ - Spec: CronJobSpec{ + oldObj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -53,6 +56,12 @@ var _ = Describe("CronJob Webhook", func() { *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} + defaulter = CronJobCustomDefaulter{ + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, + DefaultSuspend: false, + DefaultSuccessfulJobsHistoryLimit: 3, + DefaultFailedJobsHistoryLimit: 1, + } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") @@ -71,10 +80,10 @@ var _ = Describe("CronJob Webhook", func() { obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the default values are set") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") @@ -82,7 +91,7 @@ var _ = Describe("CronJob Webhook", func() { It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") - obj.Spec.ConcurrencyPolicy = ForbidConcurrent + obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = new(bool) *obj.Spec.Suspend = true obj.Spec.SuccessfulJobsHistoryLimit = new(int32) @@ -91,10 +100,10 @@ var _ = Describe("CronJob Webhook", func() { *obj.Spec.FailedJobsHistoryLimit = 2 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") diff --git a/testdata/project-v4-multigroup/api/ship/v1/webhook_suite_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go similarity index 97% rename from testdata/project-v4-multigroup/api/ship/v1/webhook_suite_test.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go index 0236bede36b..1b47dd5c702 100644 --- a/testdata/project-v4-multigroup/api/ship/v1/webhook_suite_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go @@ -30,6 +30,9 @@ import ( . "github.com/onsi/gomega" admissionv1 "k8s.io/api/admission/v1" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" + // +kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" @@ -89,7 +92,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() - err = AddToScheme(scheme) + err = batchv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1.AddToScheme(scheme) @@ -115,7 +118,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Destroyer{}).SetupWebhookWithManager(mgr) + err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_conversion.go similarity index 73% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_conversion.go index ac971d8264a..34fc09d38ee 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_conversion.go @@ -23,11 +23,12 @@ standard packages. */ import ( "fmt" - "strings" - "sigs.k8s.io/controller-runtime/pkg/conversion" + "strings" - v1 "tutorial.kubebuilder.io/project/api/v1" + batchv1 "tutorial.kubebuilder.io/project/api/v1" // Import the v1 CronJob type from the API + batchv2 "tutorial.kubebuilder.io/project/api/v2" // Import the v2 CronJob type from the API + v1 "tutorial.kubebuilder.io/project/internal/webhook/v1" // Import the hub version ) // +kubebuilder:docs-gen:collapse=Imports @@ -37,16 +38,25 @@ Our "spoke" versions need to implement the [`Convertible`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interface. Namely, they'll need `ConvertTo()` and `ConvertFrom()` methods to convert to/from the hub version. + +Wrap the v2 CronJob from the API to allow conversion to/from the hub (v1). */ /* ConvertTo is expected to modify its argument to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ -// ConvertTo converts this CronJob to the Hub version (v1). -func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1.CronJob) +// ConversionCronJobV2 wraps the API's CronJob and implements the Convertible interface. +type ConversionCronJobV2 struct { + batchv2.CronJob // Embed the original CronJob type +} + +// ConvertTo converts this v2 CronJob to the Hub version (v1.ConversionCronJob). +func (src *ConversionCronJobV2) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1.ConversionCronJob) // Use the wrapped ConversionCronJob from v1 + + // Handle the schedule conversion sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} if sched.Minute != nil { @@ -70,11 +80,12 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { The rest of the conversion is pretty rote. */ // ObjectMeta + dst.ObjectMeta = src.ObjectMeta // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = v1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) // Correct reference dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit @@ -93,19 +104,19 @@ ConvertFrom is expected to modify its receiver to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ -// ConvertFrom converts from the Hub version (v1) to this version. -func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1.CronJob) +// ConvertFrom converts from the Hub version (v1.ConversionCronJob) to this v2 CronJob. +func (dst *ConversionCronJobV2) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1.ConversionCronJob) // Use the wrapped ConversionCronJob from v1 schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { return fmt.Errorf("invalid schedule: not a standard 5-field schedule") } - partIfNeeded := func(raw string) *CronField { + partIfNeeded := func(raw string) *batchv2.CronField { if raw == "*" { return nil } - part := CronField(raw) + part := batchv2.CronField(raw) return &part } dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0]) @@ -122,7 +133,7 @@ func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv2.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) // Correct reference dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go similarity index 69% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go index 556b051a6df..8b24c999a26 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go @@ -19,31 +19,33 @@ package v2 import ( "context" "fmt" - "github.com/robfig/cron" "strings" + "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + + "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" + + batchv2 "tutorial.kubebuilder.io/project/api/v2" ) // nolint:unused // log is for logging in this package. var cronjoblog = logf.Log.WithName("cronjob-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv2.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ - DefaultConcurrencyPolicy: AllowConcurrent, + DefaultConcurrencyPolicy: batchv2.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, @@ -62,9 +64,8 @@ func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { // 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 CronJobCustomDefaulter struct { - // Default values for various CronJob fields - DefaultConcurrencyPolicy ConcurrencyPolicy + DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 @@ -74,16 +75,17 @@ var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv2.CronJob) + if !ok { return fmt.Errorf("expected an CronJob object but got %T", obj) } cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName()) // Set default values - cronjob.Default() - + d.applyDefaults(cronjob) return nil + } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -105,29 +107,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv2.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv2.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv2.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -138,65 +140,65 @@ func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime return nil, nil } -func (r *CronJob) Default() { - if r.Spec.ConcurrencyPolicy == "" { - r.Spec.ConcurrencyPolicy = AllowConcurrent +// applyDefaults applies default values to CronJob fields. +func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv2.CronJob) { + if cronJob.Spec.ConcurrencyPolicy == "" { + cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } - if r.Spec.Suspend == nil { - r.Spec.Suspend = new(bool) + if cronJob.Spec.Suspend == nil { + cronJob.Spec.Suspend = new(bool) + *cronJob.Spec.Suspend = d.DefaultSuspend } - if r.Spec.SuccessfulJobsHistoryLimit == nil { - r.Spec.SuccessfulJobsHistoryLimit = new(int32) - *r.Spec.SuccessfulJobsHistoryLimit = 3 + if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) + *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } - if r.Spec.FailedJobsHistoryLimit == nil { - r.Spec.FailedJobsHistoryLimit = new(int32) - *r.Spec.FailedJobsHistoryLimit = 1 + if cronJob.Spec.FailedJobsHistoryLimit == nil { + cronJob.Spec.FailedJobsHistoryLimit = new(int32) + *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } -func (r *CronJob) validateCronJob() error { +// validateCronJob validates the fields of a CronJob object. +func validateCronJob(cronjob *batchv2.CronJob) error { var allErrs field.ErrorList - if err := r.validateCronJobName(); err != nil { + if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } - if err := r.validateCronJobSpec(); err != nil { + if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } - - return apierrors.NewInvalid( - schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, - r.Name, allErrs) + return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } -func (r *CronJob) validateCronJobName() *field.Error { - if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { - return field.Invalid(field.NewPath("metadata").Child("name"), r.Name, "must be no more than 52 characters") +func validateCronJobName(cronjob *batchv2.CronJob) *field.Error { + if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { + return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters") } return nil } // validateCronJobSpec validates the schedule format of the custom CronSchedule type -func (r *CronJob) validateCronJobSpec() *field.Error { +func validateCronJobSpec(cronjob *batchv2.CronJob) *field.Error { // Build cron expression from the parts parts := []string{"*", "*", "*", "*", "*"} // default parts for minute, hour, day of month, month, day of week - if r.Spec.Schedule.Minute != nil { - parts[0] = string(*r.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string + if cronjob.Spec.Schedule.Minute != nil { + parts[0] = string(*cronjob.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string } - if r.Spec.Schedule.Hour != nil { - parts[1] = string(*r.Spec.Schedule.Hour) + if cronjob.Spec.Schedule.Hour != nil { + parts[1] = string(*cronjob.Spec.Schedule.Hour) } - if r.Spec.Schedule.DayOfMonth != nil { - parts[2] = string(*r.Spec.Schedule.DayOfMonth) + if cronjob.Spec.Schedule.DayOfMonth != nil { + parts[2] = string(*cronjob.Spec.Schedule.DayOfMonth) } - if r.Spec.Schedule.Month != nil { - parts[3] = string(*r.Spec.Schedule.Month) + if cronjob.Spec.Schedule.Month != nil { + parts[3] = string(*cronjob.Spec.Schedule.Month) } - if r.Spec.Schedule.DayOfWeek != nil { - parts[4] = string(*r.Spec.Schedule.DayOfWeek) + if cronjob.Spec.Schedule.DayOfWeek != nil { + parts[4] = string(*cronjob.Spec.Schedule.DayOfWeek) } // Join parts to form the full cron expression diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go similarity index 92% rename from docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook_test.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go index 0e6dffdbcab..a95db520fca 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_webhook_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go @@ -19,16 +19,18 @@ package v2 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + batchv2 "tutorial.kubebuilder.io/project/api/v2" // TODO (user): Add any additional imports if needed ) var _ = Describe("CronJob Webhook", func() { var ( - obj *CronJob + obj = &batchv2.CronJob{} ) BeforeEach(func() { - obj = &CronJob{} + obj = &batchv2.CronJob{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -66,7 +68,7 @@ var _ = Describe("CronJob Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &batchv2.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) @@ -76,7 +78,7 @@ var _ = Describe("CronJob Webhook", func() { // TODO (user): Add logic to convert the object to the desired version and verify the conversion // Example: // It("Should convert the object correctly", func() { - // convertedObj := &CronJob{} + // convertedObj := &batchv2.CronJob{} // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) // Expect(convertedObj).ToNot(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/api/ship/v2alpha1/webhook_suite_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/webhook_suite_test.go similarity index 96% rename from testdata/project-v4-multigroup/api/ship/v2alpha1/webhook_suite_test.go rename to docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/webhook_suite_test.go index 031400e44cc..08aa873ac42 100644 --- a/testdata/project-v4-multigroup/api/ship/v2alpha1/webhook_suite_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/webhook_suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2alpha1 +package v2 import ( "context" @@ -30,6 +30,9 @@ import ( . "github.com/onsi/gomega" admissionv1 "k8s.io/api/admission/v1" + + batchv2 "tutorial.kubebuilder.io/project/api/v2" + // +kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" @@ -89,7 +92,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() - err = AddToScheme(scheme) + err = batchv2.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1.AddToScheme(scheme) @@ -115,7 +118,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Cruiser{}).SetupWebhookWithManager(mgr) + err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook diff --git a/docs/book/src/multiversion-tutorial/webhooks.md b/docs/book/src/multiversion-tutorial/webhooks.md index 52f10804d97..6b383c31c52 100644 --- a/docs/book/src/multiversion-tutorial/webhooks.md +++ b/docs/book/src/multiversion-tutorial/webhooks.md @@ -14,7 +14,7 @@ setup, from when we built our defaulting and validating webhooks! ## Webhook setup... -{{#literatego ./testdata/project/api/v1/cronjob_webhook.go}} +{{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}} ## ...and `main.go` diff --git a/go.mod b/go.mod index f5ae2fd6cbe..4306240448d 100644 --- a/go.mod +++ b/go.mod @@ -18,17 +18,21 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2b91ddfd5e6..2e7ae876265 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -28,11 +29,12 @@ github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5co github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -49,8 +51,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= @@ -64,8 +66,8 @@ golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/hack/docs/internal/cronjob-tutorial/generate_cronjob.go b/hack/docs/internal/cronjob-tutorial/generate_cronjob.go index 63515ab6d63..09abfd3d3e2 100644 --- a/hack/docs/internal/cronjob-tutorial/generate_cronjob.go +++ b/hack/docs/internal/cronjob-tutorial/generate_cronjob.go @@ -375,15 +375,16 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust } func (sp *Sample) updateWebhookTests() { - file := filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook_test.go") + file := filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook_test.go") err := pluginutil.InsertCode(file, `var _ = Describe("CronJob Webhook", func() { var ( - obj *CronJob`, + obj = &batchv1.CronJob{}`, ` - oldObj *CronJob - validator CronJobCustomValidator`) + oldObj = &batchv1.CronJob{} + validator CronJobCustomValidator + defaulter CronJobCustomDefaulter`) hackutils.CheckError("insert global vars", err) err = pluginutil.ReplaceInFile(file, @@ -399,36 +400,36 @@ func (sp *Sample) updateWebhookTests() { err = pluginutil.ReplaceInFile(file, webhookTestsBeforeEachOriginal, webhookTestsBeforeEachChanged) - hackutils.CheckError("replace validating defaulting test", err) + hackutils.CheckError("replace before each webhook test ", err) } func (sp *Sample) updateWebhook() { var err error err = pluginutil.InsertCode( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `limitations under the License. */`, ` // +kubebuilder:docs-gen:collapse=Apache License`) hackutils.CheckError("fixing cronjob_webhook.go by adding collapse", err) - err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + err = pluginutil.InsertCode( + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `import ( "context" - "fmt"`, `import ( - "context" - "fmt" + "fmt"`, + ` "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" - "k8s.io/apimachinery/pkg/util/validation/field"`) + "k8s.io/apimachinery/pkg/util/validation/field"`, + ) hackutils.CheckError("add extra imports to cronjob_webhook.go", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), - `"sigs.k8s.io/controller-runtime/pkg/webhook/admission" + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), + `batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // nolint:unused @@ -437,7 +438,7 @@ func (sp *Sample) updateWebhook() { hackutils.CheckError("fixing cronjob_webhook.go", err) err = pluginutil.InsertCode( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `var cronjoblog = logf.Log.WithName("cronjob-resource")`, ` /* @@ -446,31 +447,31 @@ Then, we set up the webhook with the manager. hackutils.CheckError("fixing cronjob_webhook.go by setting webhook with manager comment", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!`, webhooksNoticeMarker) hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.`, explanationValidateCRD) hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.`, "") hackutils.CheckError("fixing cronjob_webhook.go by replace TODO to change verbs", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): Add more fields as needed for defaulting`, fragmentForDefaultFields) hackutils.CheckError("fixing cronjob_webhook.go by replacing TODO in Defaulter", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `WithDefaulter(&CronJobCustomDefaulter{}).`, `WithDefaulter(&CronJobCustomDefaulter{ - DefaultConcurrencyPolicy: AllowConcurrent, + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, @@ -478,7 +479,7 @@ Then, we set up the webhook with the manager. hackutils.CheckError("replacing WithDefaulter call in cronjob_webhook.go", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your defaulting logic. return nil @@ -486,29 +487,29 @@ Then, we set up the webhook with the manager. hackutils.CheckError("fixing cronjob_webhook.go by adding logic", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your validation logic upon object creation. return nil, nil`, - `return nil, cronjob.validateCronJob()`) + `return nil, validateCronJob(cronjob)`) hackutils.CheckError("fixing cronjob_webhook.go by fill in your validation", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your validation logic upon object update. return nil, nil`, - `return nil, cronjob.validateCronJob()`) + `return nil, validateCronJob(cronjob)`) hackutils.CheckError("fixing cronjob_webhook.go by adding validation logic upon object update", err) err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob.`, customInterfaceDefaultInfo) hackutils.CheckError("fixing cronjob_webhook.go by adding validation logic upon object update", err) err = pluginutil.AppendCodeAtTheEnd( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), webhookValidateSpecMethods) hackutils.CheckError("adding validation spec methods at the end", err) } diff --git a/hack/docs/internal/cronjob-tutorial/webhook_implementation.go b/hack/docs/internal/cronjob-tutorial/webhook_implementation.go index 46bde114dea..13cd39885fb 100644 --- a/hack/docs/internal/cronjob-tutorial/webhook_implementation.go +++ b/hack/docs/internal/cronjob-tutorial/webhook_implementation.go @@ -16,7 +16,7 @@ limitations under the License. package cronjob -const webhookIntro = `"sigs.k8s.io/controller-runtime/pkg/webhook/admission" +const webhookIntro = `batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Go imports @@ -28,25 +28,26 @@ Next, we'll setup a logger for the webhooks. ` const webhookDefaultingSettings = `// Set default values - cronjob.Default() - + d.applyDefaults(cronjob) return nil } -func (r *CronJob) Default() { - if r.Spec.ConcurrencyPolicy == "" { - r.Spec.ConcurrencyPolicy = AllowConcurrent +// applyDefaults applies default values to CronJob fields. +func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { + if cronJob.Spec.ConcurrencyPolicy == "" { + cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } - if r.Spec.Suspend == nil { - r.Spec.Suspend = new(bool) + if cronJob.Spec.Suspend == nil { + cronJob.Spec.Suspend = new(bool) + *cronJob.Spec.Suspend = d.DefaultSuspend } - if r.Spec.SuccessfulJobsHistoryLimit == nil { - r.Spec.SuccessfulJobsHistoryLimit = new(int32) - *r.Spec.SuccessfulJobsHistoryLimit = 3 + if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) + *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } - if r.Spec.FailedJobsHistoryLimit == nil { - r.Spec.FailedJobsHistoryLimit = new(int32) - *r.Spec.FailedJobsHistoryLimit = 1 + if cronJob.Spec.FailedJobsHistoryLimit == nil { + cronJob.Spec.FailedJobsHistoryLimit = new(int32) + *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } ` @@ -105,12 +106,13 @@ const webhookValidateSpecMethods = ` We validate the name and the spec of the CronJob. */ -func (r *CronJob) validateCronJob() error { +// validateCronJob validates the fields of a CronJob object. +func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList - if err := r.validateCronJobName(); err != nil { + if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } - if err := r.validateCronJobSpec(); err != nil { + if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { @@ -119,7 +121,7 @@ func (r *CronJob) validateCronJob() error { return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, - r.Name, allErrs) + cronjob.Name, allErrs) } /* @@ -132,11 +134,11 @@ declaring validation by running ` + "`" + `controller-gen crd -w` + "`" + `, or [here](/reference/markers/crd-validation.md). */ -func (r *CronJob) validateCronJobSpec() *field.Error { +func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( - r.Spec.Schedule, + cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } @@ -161,15 +163,15 @@ the apimachinery repo, so we can't declaratively validate it using the validation schema. */ -func (r *CronJob) validateCronJobName() *field.Error { - if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { +func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { + if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (` + "`" + `-$TIMESTAMP` + "`" + `) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. - return field.Invalid(field.NewPath("metadata").Child("name"), r.ObjectMeta.Name, "must be no more than 52 characters") + return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters") } return nil } @@ -178,7 +180,7 @@ func (r *CronJob) validateCronJobName() *field.Error { const fragmentForDefaultFields = ` // Default values for various CronJob fields - DefaultConcurrencyPolicy ConcurrencyPolicy + DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 @@ -201,10 +203,10 @@ const webhookTestCreateDefaultingReplaceFragment = `It("Should apply defaults wh obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the default values are set") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") @@ -212,7 +214,7 @@ const webhookTestCreateDefaultingReplaceFragment = `It("Should apply defaults wh It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") - obj.Spec.ConcurrencyPolicy = ForbidConcurrent + obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = new(bool) *obj.Spec.Suspend = true obj.Spec.SuccessfulJobsHistoryLimit = new(int32) @@ -221,10 +223,10 @@ const webhookTestCreateDefaultingReplaceFragment = `It("Should apply defaults wh *obj.Spec.FailedJobsHistoryLimit = 2 By("calling the Default method to apply defaults") - obj.Default() + defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") - Expect(obj.Spec.ConcurrencyPolicy).To(Equal(ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") + Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") @@ -246,7 +248,7 @@ const webhookTestingValidatingTodoFragment = `// TODO (user): Add logic for vali // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &batchv1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // })` @@ -303,15 +305,15 @@ const webhookTestingValidatingExampleFragment = `It("Should deny creation if the "Expected validation to pass for a valid update") })` -const webhookTestsBeforeEachOriginal = `obj = &CronJob{} +const webhookTestsBeforeEachOriginal = `obj = &batchv1.CronJob{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests` -const webhookTestsBeforeEachChanged = `obj = &CronJob{ - Spec: CronJobSpec{ +const webhookTestsBeforeEachChanged = `obj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -319,10 +321,10 @@ const webhookTestsBeforeEachChanged = `obj = &CronJob{ *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 - oldObj = &CronJob{ - Spec: CronJobSpec{ + oldObj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", - ConcurrencyPolicy: AllowConcurrent, + ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: new(int32), FailedJobsHistoryLimit: new(int32), }, @@ -331,6 +333,12 @@ const webhookTestsBeforeEachChanged = `obj = &CronJob{ *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} + defaulter = CronJobCustomDefaulter{ + DefaultConcurrencyPolicy: batchv1.AllowConcurrent, + DefaultSuspend: false, + DefaultSuccessfulJobsHistoryLimit: 3, + DefaultFailedJobsHistoryLimit: 1, + } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")` diff --git a/hack/docs/internal/multiversion-tutorial/generate_multiversion.go b/hack/docs/internal/multiversion-tutorial/generate_multiversion.go index 3ef2e9baa89..d9be3ad515c 100644 --- a/hack/docs/internal/multiversion-tutorial/generate_multiversion.go +++ b/hack/docs/internal/multiversion-tutorial/generate_multiversion.go @@ -84,7 +84,7 @@ func (sp *Sample) UpdateTutorial() { func (sp *Sample) updateWebhookV1() { err := pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, "api/v1/cronjob_webhook.go"), + filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), "Then, we set up the webhook with the manager.", `This setup doubles as setup for our conversion webhooks: as long as our types implement the @@ -107,7 +107,7 @@ func (sp *Sample) updateSampleV2() { } func (sp *Sample) createHubFiles() { - path := filepath.Join(sp.ctx.Dir, "api/v1/cronjob_conversion.go") + path := filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_conversion.go") _, err := os.Create(path) hackutils.CheckError("creating conversion file v1", err) @@ -118,7 +118,7 @@ func (sp *Sample) createHubFiles() { err = pluginutil.AppendCodeAtTheEnd(path, hubV1Code) hackutils.CheckError("appending hubV1Code to cronjob_conversion.go", err) - path = filepath.Join(sp.ctx.Dir, "api/v2/cronjob_conversion.go") + path = filepath.Join(sp.ctx.Dir, "internal/webhook/v2/cronjob_conversion.go") _, err = os.Create(path) hackutils.CheckError("creating conversion file v2", err) @@ -202,36 +202,21 @@ func (sp *Sample) updateApiV1() { } func (sp *Sample) updateWebhookV2() { - path := "api/v2/cronjob_webhook.go" + path := "internal/webhook/v2/cronjob_webhook.go" - err := pluginutil.ReplaceInFile( + err := pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `import ( "context" - "fmt" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -)`, - `import ( - "context" - "fmt" - "github.com/robfig/cron" + "fmt"`, + ` "strings" - + + "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" - "k8s.io/apimachinery/pkg/util/validation/field" - 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" -)`, + "k8s.io/apimachinery/pkg/util/validation/field"`, ) hackutils.CheckError("replacing imports in v2", err) @@ -244,57 +229,35 @@ func (sp *Sample) updateWebhookV2() { err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), - `// TODO(user): fill in your defaulting logic.`, + `// TODO(user): fill in your defaulting logic. + + return nil`, cronJobDefaultingLogic, ) hackutils.CheckError("replacing defaulting logic in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), - `// TODO(user): fill in your validation logic upon object creation.`, - `return nil, cronjob.validateCronJob()`, - ) - hackutils.CheckError("replacing validation logic for creation in v2", err) + `// TODO(user): fill in your validation logic upon object creation. - err = pluginutil.ReplaceInFile( - filepath.Join(sp.ctx.Dir, path), - `// TODO(user): fill in your validation logic upon object update.`, - `return nil, cronjob.validateCronJob()`, + return nil, nil`, + `return nil, validateCronJob(cronjob)`, ) - hackutils.CheckError("replacing validation logic for update in v2", err) + hackutils.CheckError("replacing validation logic for creation in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), - `return nil, cronjob.validateCronJob() + `// TODO(user): fill in your validation logic upon object update. return nil, nil`, - `return nil, cronjob.validateCronJob()`, + `return nil, validateCronJob(cronjob)`, ) - hackutils.CheckError("fixing ValidateCreate in v2", err) + hackutils.CheckError("replacing validation logic for update in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), - `// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). - WithValidator(&CronJobCustomValidator{}). - WithDefaulter(&CronJobCustomDefaulter{}). - Complete() -}`, - `// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). - WithValidator(&CronJobCustomValidator{}). - WithDefaulter(&CronJobCustomDefaulter{ - DefaultConcurrencyPolicy: AllowConcurrent, - DefaultSuspend: false, - DefaultSuccessfulJobsHistoryLimit: 3, - DefaultFailedJobsHistoryLimit: 1, - }). - Complete() -}`, + originalSetupManager, + replaceSetupManager, ) hackutils.CheckError("replacing SetupWebhookWithManager in v2", err) diff --git a/hack/docs/internal/multiversion-tutorial/hub.go b/hack/docs/internal/multiversion-tutorial/hub.go index e28dd16e131..4e3618d1956 100644 --- a/hack/docs/internal/multiversion-tutorial/hub.go +++ b/hack/docs/internal/multiversion-tutorial/hub.go @@ -41,8 +41,22 @@ method called ` + "`Hub()`" + ` to serve as a We could also just put this inline in our ` + "`cronjob_types.go`" + ` file. */ +import ( + batchv1 "tutorial.kubebuilder.io/project/api/v1" // Import CronJob from API package +) + +/* +ConversionCronJob wraps the API's CronJob and implements the Hub interface for conversion. +*/ + +// ConversionCronJob wraps the existing CronJob type from the API. +type ConversionCronJob struct { + batchv1.CronJob // Embed the original CronJob type +} + // Hub marks this type as a conversion hub. -func (*CronJob) Hub() {}` +func (*ConversionCronJob) Hub() {} +` const hubV2Code = `/* Licensed under the Apache License, Version 2.0 (the "License"); @@ -69,11 +83,12 @@ standard packages. */ import ( "fmt" - "strings" - "sigs.k8s.io/controller-runtime/pkg/conversion" + "strings" - v1 "tutorial.kubebuilder.io/project/api/v1" + batchv1 "tutorial.kubebuilder.io/project/api/v1" // Import the v1 CronJob type from the API + batchv2 "tutorial.kubebuilder.io/project/api/v2" // Import the v2 CronJob type from the API + v1 "tutorial.kubebuilder.io/project/internal/webhook/v1" // Import the hub version ) // +kubebuilder:docs-gen:collapse=Imports @@ -83,16 +98,25 @@ Our "spoke" versions need to implement the [` + "`" + `Convertible` + "`" + `](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interface. Namely, they'll need ` + "`ConvertTo()`" + ` and ` + "`ConvertFrom()`" + ` methods to convert to/from the hub version. + +Wrap the v2 CronJob from the API to allow conversion to/from the hub (v1). */ /* ConvertTo is expected to modify its argument to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ -// ConvertTo converts this CronJob to the Hub version (v1). -func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1.CronJob) +// ConversionCronJobV2 wraps the API's CronJob and implements the Convertible interface. +type ConversionCronJobV2 struct { + batchv2.CronJob // Embed the original CronJob type +} + +// ConvertTo converts this v2 CronJob to the Hub version (v1.ConversionCronJob). +func (src *ConversionCronJobV2) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1.ConversionCronJob) // Use the wrapped ConversionCronJob from v1 + + // Handle the schedule conversion sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} if sched.Minute != nil { @@ -116,11 +140,12 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { The rest of the conversion is pretty rote. */ // ObjectMeta - dst.ObjectMeta = src.ObjectMeta + dst.ObjectMeta = src.ObjectMeta + // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = v1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) // Correct reference dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit @@ -139,19 +164,19 @@ ConvertFrom is expected to modify its receiver to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ -// ConvertFrom converts from the Hub version (v1) to this version. -func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1.CronJob) +// ConvertFrom converts from the Hub version (v1.ConversionCronJob) to this v2 CronJob. +func (dst *ConversionCronJobV2) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1.ConversionCronJob) // Use the wrapped ConversionCronJob from v1 schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { return fmt.Errorf("invalid schedule: not a standard 5-field schedule") } - partIfNeeded := func(raw string) *CronField { + partIfNeeded := func(raw string) *batchv2.CronField { if raw == "*" { return nil } - part := CronField(raw) + part := batchv2.CronField(raw) return &part } dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0]) @@ -168,7 +193,7 @@ func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv2.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) // Correct reference dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit @@ -180,4 +205,5 @@ func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { // +kubebuilder:docs-gen:collapse=rote conversion return nil -}` +} +` diff --git a/hack/docs/internal/multiversion-tutorial/webhook_v2_implementaton.go b/hack/docs/internal/multiversion-tutorial/webhook_v2_implementaton.go index 3329a4eb7c2..0dd5ad24f69 100644 --- a/hack/docs/internal/multiversion-tutorial/webhook_v2_implementaton.go +++ b/hack/docs/internal/multiversion-tutorial/webhook_v2_implementaton.go @@ -16,81 +16,80 @@ limitations under the License. package multiversion -const cronJobFieldsForDefaulting = ` -// Default values for various CronJob fields -DefaultConcurrencyPolicy ConcurrencyPolicy -DefaultSuspend bool -DefaultSuccessfulJobsHistoryLimit int32 -DefaultFailedJobsHistoryLimit int32 +const cronJobFieldsForDefaulting = ` // Default values for various CronJob fields + DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy + DefaultSuspend bool + DefaultSuccessfulJobsHistoryLimit int32 + DefaultFailedJobsHistoryLimit int32 ` -const cronJobDefaultingLogic = ` -// Set default values -cronjob.Default() +const cronJobDefaultingLogic = `// Set default values + d.applyDefaults(cronjob) + return nil ` const cronJobDefaultFunction = ` -func (r *CronJob) Default() { - if r.Spec.ConcurrencyPolicy == "" { - r.Spec.ConcurrencyPolicy = AllowConcurrent +// applyDefaults applies default values to CronJob fields. +func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv2.CronJob) { + if cronJob.Spec.ConcurrencyPolicy == "" { + cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } - if r.Spec.Suspend == nil { - r.Spec.Suspend = new(bool) + if cronJob.Spec.Suspend == nil { + cronJob.Spec.Suspend = new(bool) + *cronJob.Spec.Suspend = d.DefaultSuspend } - if r.Spec.SuccessfulJobsHistoryLimit == nil { - r.Spec.SuccessfulJobsHistoryLimit = new(int32) - *r.Spec.SuccessfulJobsHistoryLimit = 3 + if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) + *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } - if r.Spec.FailedJobsHistoryLimit == nil { - r.Spec.FailedJobsHistoryLimit = new(int32) - *r.Spec.FailedJobsHistoryLimit = 1 + if cronJob.Spec.FailedJobsHistoryLimit == nil { + cronJob.Spec.FailedJobsHistoryLimit = new(int32) + *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } ` const cronJobValidationFunction = ` -func (r *CronJob) validateCronJob() error { +// validateCronJob validates the fields of a CronJob object. +func validateCronJob(cronjob *batchv2.CronJob) error { var allErrs field.ErrorList - if err := r.validateCronJobName(); err != nil { + if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } - if err := r.validateCronJobSpec(); err != nil { + if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } - - return apierrors.NewInvalid( - schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, - r.Name, allErrs) + return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } -func (r *CronJob) validateCronJobName() *field.Error { - if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { - return field.Invalid(field.NewPath("metadata").Child("name"), r.Name, "must be no more than 52 characters") +func validateCronJobName(cronjob *batchv2.CronJob) *field.Error { + if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 { + return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters") } return nil } // validateCronJobSpec validates the schedule format of the custom CronSchedule type -func (r *CronJob) validateCronJobSpec() *field.Error { +func validateCronJobSpec(cronjob *batchv2.CronJob) *field.Error { // Build cron expression from the parts parts := []string{"*", "*", "*", "*", "*"} // default parts for minute, hour, day of month, month, day of week - if r.Spec.Schedule.Minute != nil { - parts[0] = string(*r.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string + if cronjob.Spec.Schedule.Minute != nil { + parts[0] = string(*cronjob.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string } - if r.Spec.Schedule.Hour != nil { - parts[1] = string(*r.Spec.Schedule.Hour) + if cronjob.Spec.Schedule.Hour != nil { + parts[1] = string(*cronjob.Spec.Schedule.Hour) } - if r.Spec.Schedule.DayOfMonth != nil { - parts[2] = string(*r.Spec.Schedule.DayOfMonth) + if cronjob.Spec.Schedule.DayOfMonth != nil { + parts[2] = string(*cronjob.Spec.Schedule.DayOfMonth) } - if r.Spec.Schedule.Month != nil { - parts[3] = string(*r.Spec.Schedule.Month) + if cronjob.Spec.Schedule.Month != nil { + parts[3] = string(*cronjob.Spec.Schedule.Month) } - if r.Spec.Schedule.DayOfWeek != nil { - parts[4] = string(*r.Spec.Schedule.DayOfWeek) + if cronjob.Spec.Schedule.DayOfWeek != nil { + parts[4] = string(*cronjob.Spec.Schedule.DayOfWeek) } // Join parts to form the full cron expression @@ -108,3 +107,24 @@ func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { return nil } ` + +const originalSetupManager = `// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv2.CronJob{}). + WithValidator(&CronJobCustomValidator{}). + WithDefaulter(&CronJobCustomDefaulter{}). + Complete() +}` + +const replaceSetupManager = `// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv2.CronJob{}). + WithValidator(&CronJobCustomValidator{}). + WithDefaulter(&CronJobCustomDefaulter{ + DefaultConcurrencyPolicy: batchv2.AllowConcurrent, + DefaultSuspend: false, + DefaultSuccessfulJobsHistoryLimit: 3, + DefaultFailedJobsHistoryLimit: 1, + }). + Complete() +}` diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go index ceb4876051b..7d1599deb54 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go @@ -54,7 +54,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go index 031a83e9795..fb364cf6766 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go @@ -62,6 +62,12 @@ type MainUpdater struct { //nolint:maligned // Flags to indicate which parts need to be included when updating the file WireResource, WireController, WireWebhook bool + + // Deprecated - The flag should be removed from go/v5 + // IsLegacyPath indicates if webhooks should be scaffolded under the API. + // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback. + // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path. + IsLegacyPath bool } // GetPath implements file.Builder @@ -93,6 +99,10 @@ const ( apiImportCodeFragment = `%s "%s" ` controllerImportCodeFragment = `"%s/internal/controller" +` + webhookImportCodeFragment = `%s "%s/internal/webhook/%s" +` + multiGroupWebhookImportCodeFragment = `%s "%s/internal/webhook/%s/%s" ` multiGroupControllerImportCodeFragment = `%scontroller "%s/internal/controller/%s" ` @@ -114,7 +124,7 @@ const ( os.Exit(1) } ` - webhookSetupCodeFragment = `// nolint:goconst + webhookSetupCodeFragmentLegacy = `// nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err = (&%s.%s{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "%s") @@ -122,6 +132,15 @@ const ( } } ` + + webhookSetupCodeFragment = `// nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = %s.Setup%sWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "%s") + os.Exit(1) + } + } +` ) // GetCodeFragments implements file.Inserter @@ -138,6 +157,15 @@ func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { if f.WireResource { imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } + if f.WireWebhook && !f.IsLegacyPath { + if !f.MultiGroup || f.Resource.Group == "" { + imports = append(imports, fmt.Sprintf(webhookImportCodeFragment, + "webhook"+f.Resource.ImportAlias(), f.Repo, f.Resource.Version)) + } else { + imports = append(imports, fmt.Sprintf(multiGroupWebhookImportCodeFragment, + "webhook"+f.Resource.ImportAlias(), f.Repo, f.Resource.Group, f.Resource.Version)) + } + } if f.WireController { if !f.MultiGroup || f.Resource.Group == "" { @@ -166,8 +194,13 @@ func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { } } if f.WireWebhook { - setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment, - f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind)) + if f.IsLegacyPath { + setup = append(setup, fmt.Sprintf(webhookSetupCodeFragmentLegacy, + f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind)) + } else { + setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment, + "webhook"+f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind)) + } } // Only store code fragments in the map if the slices are non-empty diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go similarity index 77% rename from pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook.go rename to pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go index 25b6ae1d830..584ddf036b7 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package api +package webhooks import ( "path/filepath" @@ -41,15 +41,30 @@ type Webhook struct { // nolint:maligned AdmissionReviewVersions string Force bool + + // Deprecated - The flag should be removed from go/v5 + // IsLegacyPath indicates if webhooks should be scaffolded under the API. + // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback. + // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path. + IsLegacyPath bool } // SetTemplateDefaults implements file.Template func (f *Webhook) SetTemplateDefaults() error { if f.Path == "" { - if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook.go") + if f.IsLegacyPath { + // Deprecated code - TODO: remove me for go/v5 + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook.go") + } else { + f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook.go") + } } else { - f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook.go") + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("internal", "webhook", "%[group]", "%[version]", "%[kind]_webhook.go") + } else { + f.Path = filepath.Join("internal", "webhook", "%[version]", "%[kind]_webhook.go") + } } } @@ -97,12 +112,18 @@ import ( {{- if .Resource.HasValidationWebhook }} "sigs.k8s.io/controller-runtime/pkg/webhook/admission" {{- end }} + {{ if not .IsLegacyPath -}} + {{ if not (isEmptyStr .Resource.Path) -}} + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" + {{- end }} + {{- end }} ) // nolint:unused // log is for logging in this package. var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource") +{{- if .IsLegacyPath -}} // SetupWebhookWithManager will setup the manager to manage the webhooks. func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). @@ -115,6 +136,24 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { {{- end }} Complete() } +{{- else }} +// Setup{{ .Resource.Kind }}WebhookWithManager registers the webhook for {{ .Resource.Kind }} in the manager. +func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + {{- if not (isEmptyStr .Resource.ImportAlias) -}} + For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}). + {{- else -}} + For(&{{ .Resource.Kind }}{}). + {{- end }} + {{- if .Resource.HasValidationWebhook }} + WithValidator(&{{ .Resource.Kind }}CustomValidator{}). + {{- end }} + {{- if .Resource.HasDefaultingWebhook }} + WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}). + {{- end }} + Complete() +} +{{- end }} // TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! ` @@ -137,7 +176,12 @@ var _ webhook.CustomDefaulter = &{{ .Resource.Kind }}CustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind {{ .Resource.Kind }}. func (d *{{ .Resource.Kind }}CustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + {{- if .IsLegacyPath -}} {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }}) + {{- else }} + {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + {{- end }} + if !ok { return fmt.Errorf("expected an {{ .Resource.Kind }} object but got %T", obj) } @@ -170,7 +214,11 @@ var _ webhook.CustomValidator = &{{ .Resource.Kind }}CustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}. func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + {{- if .IsLegacyPath -}} {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }}) + {{- else }} + {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + {{- end }} if !ok { return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", obj) } @@ -183,9 +231,13 @@ func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(ctx context.Context // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}. func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + {{- if .IsLegacyPath -}} {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.Kind }}) + {{- else }} + {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + {{- end }} if !ok { - return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", newObj) + return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object for the newObj but got %T", newObj) } {{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon update", "name", {{ lower .Resource.Kind }}.GetName()) @@ -196,7 +248,11 @@ func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(ctx context.Context // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}. func (v *{{ .Resource.Kind }}CustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + {{- if .IsLegacyPath -}} {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }}) + {{- else }} + {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + {{- end }} if !ok { return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", obj) } diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_suitetest.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go similarity index 76% rename from pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_suitetest.go rename to pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go index a058a37ef91..bdea8671172 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_suitetest.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package api +package webhooks import ( "fmt" @@ -44,15 +44,30 @@ type WebhookSuite struct { //nolint:maligned // BaseDirectoryRelativePath define the Path for the base directory when it is multigroup BaseDirectoryRelativePath string + + // Deprecated - The flag should be removed from go/v5 + // IsLegacyPath indicates if webhooks should be scaffolded under the API. + // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback. + // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path. + IsLegacyPath bool } // SetTemplateDefaults implements file.Template func (f *WebhookSuite) SetTemplateDefaults() error { if f.Path == "" { - if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "webhook_suite_test.go") + if f.IsLegacyPath { + // Deprecated code - TODO: remove me for go/v5 + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("api", "%[group]", "%[version]", "webhook_suite_test.go") + } else { + f.Path = filepath.Join("api", "%[version]", "webhook_suite_test.go") + } } else { - f.Path = filepath.Join("api", "%[version]", "webhook_suite_test.go") + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("internal", "webhook", "%[group]", "%[version]", "webhook_suite_test.go") + } else { + f.Path = filepath.Join("internal", "webhook", "%[version]", "webhook_suite_test.go") + } } } @@ -61,17 +76,25 @@ func (f *WebhookSuite) SetTemplateDefaults() error { f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate, machinery.NewMarkerFor(f.Path, importMarker), - admissionImportAlias, + f.Resource.ImportAlias(), admissionImportAlias, machinery.NewMarkerFor(f.Path, addSchemeMarker), machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), "%s", "%d", ) - // If is multigroup the path needs to be ../../.. since it has the group dir. - f.BaseDirectoryRelativePath = `"..", ".."` - if f.MultiGroup && f.Resource.Group != "" { - f.BaseDirectoryRelativePath = `"..", "..",".."` + if f.IsLegacyPath { + // If is multigroup the path needs to be ../../../ since it has the group dir. + f.BaseDirectoryRelativePath = `"..", ".."` + if f.MultiGroup && f.Resource.Group != "" { + f.BaseDirectoryRelativePath = `"..", "..", ".."` + } + } else { + // If is multigroup the path needs to be ../../../../ since it has the group dir. + f.BaseDirectoryRelativePath = `"..", "..", ".."` + if f.MultiGroup && f.Resource.Group != "" { + f.BaseDirectoryRelativePath = `"..", "..", "..", ".."` + } } return nil @@ -98,7 +121,14 @@ const ( apiImportCodeFragment = `%s "%s" ` - addWebhookManagerCodeFragment = `err = (&%s{}).SetupWebhookWithManager(mgr) + // Deprecated - TODO: remove for go/v5 + // addWebhookManagerCodeFragmentLegacy is for the path under API + addWebhookManagerCodeFragmentLegacy = `err = (&%s{}).SetupWebhookWithManager(mgr) +Expect(err).NotTo(HaveOccurred()) + +` + + addWebhookManagerCodeFragment = `err = Setup%sWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) ` @@ -110,6 +140,9 @@ func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) + if !f.IsLegacyPath { + imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) + } imports = append(imports, fmt.Sprintf(apiImportCodeFragment, admissionImportAlias, admissionPath)) // Generate add scheme code fragments @@ -117,7 +150,11 @@ func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap { // Generate add webhookManager code fragments addWebhookManager := make([]string, 0) - addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragment, f.Resource.Kind)) + if f.IsLegacyPath { + addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragmentLegacy, f.Resource.Kind)) + } else { + addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragment, f.Resource.Kind)) + } // Only store code fragments in the map if the slices are non-empty if len(addWebhookManager) != 0 { @@ -208,7 +245,11 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() + {{- if .IsLegacyPath -}} err = AddToScheme(scheme) + {{- else }} + err = %s.AddToScheme(scheme) + {{- end }} Expect(err).NotTo(HaveOccurred()) err = %s.AddToScheme(scheme) diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_test_template.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go similarity index 72% rename from pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_test_template.go rename to pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go index 162eaaa9d06..818227d6597 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/webhook_test_template.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package api +package webhooks import ( "fmt" @@ -36,15 +36,30 @@ type WebhookTest struct { // nolint:maligned machinery.ResourceMixin Force bool + + // Deprecated - The flag should be removed from go/v5 + // IsLegacyPath indicates if webhooks should be scaffolded under the API. + // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback. + // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path. + IsLegacyPath bool } // SetTemplateDefaults implements file.Template func (f *WebhookTest) SetTemplateDefaults() error { if f.Path == "" { - if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook_test.go") + if f.IsLegacyPath { + // Deprecated code - TODO: remove me for go/v5 + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook_test.go") + } else { + f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook_test.go") + } } else { - f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook_test.go") + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("internal", "webhook", "%[group]", "%[version]", "%[kind]_webhook_test.go") + } else { + f.Path = filepath.Join("internal", "webhook", "%[version]", "%[kind]_webhook_test.go") + } } } f.Path = f.Resource.Replacer().Replace(f.Path) @@ -78,16 +93,29 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + {{ if not .IsLegacyPath -}} + {{ if not (isEmptyStr .Resource.Path) -}} + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" + {{- end }} + {{- end }} // TODO (user): Add any additional imports if needed ) var _ = Describe("{{ .Resource.Kind }} Webhook", func() { var ( - obj *{{ .Resource.Kind }} + {{- if .IsLegacyPath -}} + obj = &{{ .Resource.Kind }}{} + {{- else }} + obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + {{- end }} ) BeforeEach(func() { + {{- if .IsLegacyPath -}} obj = &{{ .Resource.Kind }}{} + {{- else }} + obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + {{- end }} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -106,7 +134,11 @@ Context("When creating {{ .Resource.Kind }} under Conversion Webhook", func() { // TODO (user): Add logic to convert the object to the desired version and verify the conversion // Example: // It("Should convert the object correctly", func() { + {{- if .IsLegacyPath -}} // convertedObj := &{{ .Resource.Kind }}{} + {{- else }} + // convertedObj := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + {{- end }} // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) // Expect(convertedObj).ToNot(BeNil()) // }) @@ -131,7 +163,11 @@ Context("When creating or updating {{ .Resource.Kind }} under Validating Webhook // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") + {{- if .IsLegacyPath -}} // oldObj := &Captain{SomeRequiredField: "valid_value"} + {{- else }} + // oldObj := &{{ .Resource.ImportAlias }}.Captain{SomeRequiredField: "valid_value"} + {{- end }} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/pkg/plugins/golang/v4/scaffolds/webhook.go b/pkg/plugins/golang/v4/scaffolds/webhook.go index 20f4ac5953b..a321fb71481 100644 --- a/pkg/plugins/golang/v4/scaffolds/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/webhook.go @@ -25,11 +25,12 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" + pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates" - "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e" + "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks" ) var _ plugins.Scaffolder = &webhookScaffolder{} @@ -43,14 +44,20 @@ type webhookScaffolder struct { // force indicates whether to scaffold controller files even if it exists or not force bool + + // Deprecated - TODO: remove it for go/v5 + // isLegacy indicates that the resource should be created in the legacy path under the api + isLegacy bool } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations -func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) plugins.Scaffolder { +func NewWebhookScaffolder(config config.Config, resource resource.Resource, + force bool, isLegacy bool) plugins.Scaffolder { return &webhookScaffolder{ config: config, resource: resource, force: force, + isLegacy: isLegacy, } } @@ -86,10 +93,10 @@ func (s *webhookScaffolder) Scaffold() error { } if err := scaffold.Execute( - &api.Webhook{Force: s.force}, + &webhooks.Webhook{Force: s.force, IsLegacyPath: s.isLegacy}, &e2e.WebhookTestUpdater{WireWebhook: true}, - &templates.MainUpdater{WireWebhook: true}, - &api.WebhookTest{Force: s.force}, + &templates.MainUpdater{WireWebhook: true, IsLegacyPath: s.isLegacy}, + &webhooks.WebhookTest{Force: s.force, IsLegacyPath: s.isLegacy}, ); err != nil { return err } @@ -102,11 +109,24 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest. if doDefaulting || doValidation { if err := scaffold.Execute( - &api.WebhookSuite{K8SVersion: EnvtestK8SVersion}, + &webhooks.WebhookSuite{K8SVersion: EnvtestK8SVersion, IsLegacyPath: s.isLegacy}, ); err != nil { return err } } + // TODO: remove for go/v5 + if !s.isLegacy { + if hasInternalController, err := pluginutil.HasFragment("Dockerfile", "internal/controller"); err != nil { + log.Error("Unable to read Dockerfile to check if webhook(s) will be properly copied: ", err) + } else if hasInternalController { + log.Warning("Dockerfile is copying internal/controller. To allow copying webhooks, " + + "it will be edited, and `internal/controller` will be replaced by `internal/`.") + + if err := pluginutil.ReplaceInFile("Dockerfile", "internal/controller", "internal/"); err != nil { + log.Error("Unable to replace \"internal/controller\" with \"internal/\" in the Dockerfile: ", err) + } + } + } return nil } diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go index 9fe89cb3343..a78ddff850e 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -43,6 +43,10 @@ type createWebhookSubcommand struct { // force indicates that the resource should be created even if it already exists force bool + + // Deprecated - TODO: remove it for go/v5 + // isLegacyPath indicates that the resource should be created in the legacy path under the api + isLegacyPath bool } func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { @@ -73,6 +77,11 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.options.DoConversion, "conversion", false, "if set, scaffold the conversion webhook") + // TODO: remove for go/v5 + fs.BoolVar(&p.isLegacyPath, "legacy", false, + "[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+ + "This option will be removed in future versions.") + fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") } @@ -107,7 +116,7 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { } func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error { - scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force) + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force, p.isLegacyPath) scaffolder.InjectFS(fs) return scaffolder.Scaffold() } diff --git a/test/e2e/v4/generate_test.go b/test/e2e/v4/generate_test.go index d02f71f4aa2..6d235313d04 100644 --- a/test/e2e/v4/generate_test.go +++ b/test/e2e/v4/generate_test.go @@ -50,7 +50,7 @@ func GenerateV4(kbc *utils.TestContext) { By("implementing the mutating and validating webhooks") webhookFilePath := filepath.Join( - kbc.Dir, "api", kbc.Version, + kbc.Dir, "internal/webhook", kbc.Version, fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind))) err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -91,7 +91,7 @@ func GenerateV4WithoutMetrics(kbc *utils.TestContext) { By("implementing the mutating and validating webhooks") webhookFilePath := filepath.Join( - kbc.Dir, "api", kbc.Version, + kbc.Dir, "internal/webhook", kbc.Version, fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind))) err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -155,7 +155,7 @@ func GenerateV4WithNetworkPolicies(kbc *utils.TestContext) { By("implementing the mutating and validating webhooks") webhookFilePath := filepath.Join( - kbc.Dir, "api", kbc.Version, + kbc.Dir, "internal/webhook", kbc.Version, fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind))) err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind)) ExpectWithOffset(1, err).NotTo(HaveOccurred()) diff --git a/testdata/project-v4-multigroup/Dockerfile b/testdata/project-v4-multigroup/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/testdata/project-v4-multigroup/Dockerfile +++ b/testdata/project-v4-multigroup/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/testdata/project-v4-multigroup/api/crew/v1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/crew/v1/zz_generated.deepcopy.go index 438b50de573..26925504f6f 100644 --- a/testdata/project-v4-multigroup/api/crew/v1/zz_generated.deepcopy.go +++ b/testdata/project-v4-multigroup/api/crew/v1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go index a41c7b842d1..6254bdb0507 100644 --- a/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go +++ b/testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4-multigroup/api/ship/v1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/ship/v1/zz_generated.deepcopy.go index ca3974a1d81..8931c51a317 100644 --- a/testdata/project-v4-multigroup/api/ship/v1/zz_generated.deepcopy.go +++ b/testdata/project-v4-multigroup/api/ship/v1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4-multigroup/api/ship/v2alpha1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/ship/v2alpha1/zz_generated.deepcopy.go index 8f391c1cf27..01af43ca4fe 100644 --- a/testdata/project-v4-multigroup/api/ship/v2alpha1/zz_generated.deepcopy.go +++ b/testdata/project-v4-multigroup/api/ship/v2alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v2alpha1 import ( - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index e7dc7e0a55e..526a338c760 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -53,6 +53,11 @@ 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" + 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" + webhookshipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/ship/v1beta1" + webhookshipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1" // +kubebuilder:scaffold:imports ) @@ -178,7 +183,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&crewv1.Captain{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookcrewv1.SetupCaptainWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Captain") os.Exit(1) } @@ -192,7 +197,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&shipv1beta1.Frigate{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookshipv1beta1.SetupFrigateWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Frigate") os.Exit(1) } @@ -206,7 +211,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&shipv1.Destroyer{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookshipv1.SetupDestroyerWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Destroyer") os.Exit(1) } @@ -220,7 +225,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&shipv2alpha1.Cruiser{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookshipv2alpha1.SetupCruiserWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Cruiser") os.Exit(1) } @@ -285,7 +290,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&examplecomv1alpha1.Memcached{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookexamplecomv1alpha1.SetupMemcachedWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Memcached") os.Exit(1) } diff --git a/testdata/project-v4-multigroup/api/crew/v1/captain_webhook.go b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go similarity index 90% rename from testdata/project-v4-multigroup/api/crew/v1/captain_webhook.go rename to testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go index 98fc273afc7..acb0f66d290 100644 --- a/testdata/project-v4-multigroup/api/crew/v1/captain_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go @@ -25,16 +25,17 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1" ) // nolint:unused // log is for logging in this package. var captainlog = logf.Log.WithName("captain-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Captain) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCaptainWebhookWithManager registers the webhook for Captain in the manager. +func SetupCaptainWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.Captain{}). WithValidator(&CaptainCustomValidator{}). WithDefaulter(&CaptainCustomDefaulter{}). Complete() @@ -58,7 +59,8 @@ var _ webhook.CustomDefaulter = &CaptainCustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Captain. func (d *CaptainCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) + if !ok { return fmt.Errorf("expected an Captain object but got %T", obj) } @@ -88,7 +90,7 @@ var _ webhook.CustomValidator = &CaptainCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) if !ok { return nil, fmt.Errorf("expected a Captain object but got %T", obj) } @@ -101,9 +103,9 @@ func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - captain, ok := newObj.(*Captain) + captain, ok := newObj.(*crewv1.Captain) if !ok { - return nil, fmt.Errorf("expected a Captain object but got %T", newObj) + return nil, fmt.Errorf("expected a Captain object for the newObj but got %T", newObj) } captainlog.Info("Validation for Captain upon update", "name", captain.GetName()) @@ -114,7 +116,7 @@ func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) if !ok { return nil, fmt.Errorf("expected a Captain object but got %T", obj) } diff --git a/testdata/project-v4/api/v1/captain_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook_test.go similarity index 92% rename from testdata/project-v4/api/v1/captain_webhook_test.go rename to testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook_test.go index 4c1020c9c56..cbabe98e3c7 100644 --- a/testdata/project-v4/api/v1/captain_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook_test.go @@ -19,16 +19,18 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Captain Webhook", func() { var ( - obj *Captain + obj = &crewv1.Captain{} ) BeforeEach(func() { - obj = &Captain{} + obj = &crewv1.Captain{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -66,7 +68,7 @@ var _ = Describe("Captain Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &crewv1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/internal/webhook/crew/v1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/crew/v1/webhook_suite_test.go new file mode 100644 index 00000000000..21e07938ca4 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/crew/v1/webhook_suite_test.go @@ -0,0 +1,150 @@ +/* +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" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/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 = crewv1.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 = SetupCaptainWebhookWithManager(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-with-plugins/api/v1alpha1/memcached_webhook.go b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go similarity index 86% rename from testdata/project-v4-with-plugins/api/v1alpha1/memcached_webhook.go rename to testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go index 11f098e7358..8175798e60f 100644 --- a/testdata/project-v4-with-plugins/api/v1alpha1/memcached_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go @@ -25,16 +25,17 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" ) // nolint:unused // log is for logging in this package. var memcachedlog = logf.Log.WithName("memcached-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupMemcachedWebhookWithManager registers the webhook for Memcached in the manager. +func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&examplecomv1alpha1.Memcached{}). WithValidator(&MemcachedCustomValidator{}). Complete() } @@ -60,7 +61,7 @@ var _ webhook.CustomValidator = &MemcachedCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - memcached, ok := obj.(*Memcached) + memcached, ok := obj.(*examplecomv1alpha1.Memcached) if !ok { return nil, fmt.Errorf("expected a Memcached object but got %T", obj) } @@ -73,9 +74,9 @@ func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runti // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - memcached, ok := newObj.(*Memcached) + memcached, ok := newObj.(*examplecomv1alpha1.Memcached) if !ok { - return nil, fmt.Errorf("expected a Memcached object but got %T", newObj) + return nil, fmt.Errorf("expected a Memcached object for the newObj but got %T", newObj) } memcachedlog.Info("Validation for Memcached upon update", "name", memcached.GetName()) @@ -86,7 +87,7 @@ func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, n // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - memcached, ok := obj.(*Memcached) + memcached, ok := obj.(*examplecomv1alpha1.Memcached) if !ok { return nil, fmt.Errorf("expected a Memcached object but got %T", obj) } diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook_test.go similarity index 87% rename from testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook_test.go rename to testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook_test.go index b966fb2d8da..2de84354f39 100644 --- a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook_test.go @@ -19,16 +19,18 @@ package v1alpha1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Memcached Webhook", func() { var ( - obj *Memcached + obj = &examplecomv1alpha1.Memcached{} ) BeforeEach(func() { - obj = &Memcached{} + obj = &examplecomv1alpha1.Memcached{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -55,7 +57,7 @@ var _ = Describe("Memcached Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &examplecomv1alpha1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/webhook_suite_test.go new file mode 100644 index 00000000000..18d7d16a186 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/webhook_suite_test.go @@ -0,0 +1,150 @@ +/* +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 v1alpha1 + +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" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" + + // +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 = examplecomv1alpha1.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 = SetupMemcachedWebhookWithManager(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-multigroup/api/ship/v1/destroyer_webhook.go b/testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook.go similarity index 87% rename from testdata/project-v4-multigroup/api/ship/v1/destroyer_webhook.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook.go index dbc040c9dbc..128306dc9cb 100644 --- a/testdata/project-v4-multigroup/api/ship/v1/destroyer_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook.go @@ -24,16 +24,17 @@ import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1" ) // nolint:unused // log is for logging in this package. var destroyerlog = logf.Log.WithName("destroyer-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Destroyer) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupDestroyerWebhookWithManager registers the webhook for Destroyer in the manager. +func SetupDestroyerWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&shipv1.Destroyer{}). WithDefaulter(&DestroyerCustomDefaulter{}). Complete() } @@ -56,7 +57,8 @@ var _ webhook.CustomDefaulter = &DestroyerCustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Destroyer. func (d *DestroyerCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - destroyer, ok := obj.(*Destroyer) + destroyer, ok := obj.(*shipv1.Destroyer) + if !ok { return fmt.Errorf("expected an Destroyer object but got %T", obj) } diff --git a/testdata/project-v4-multigroup/api/ship/v1/destroyer_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook_test.go similarity index 91% rename from testdata/project-v4-multigroup/api/ship/v1/destroyer_webhook_test.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook_test.go index 4cdedb2e959..0d04c778271 100644 --- a/testdata/project-v4-multigroup/api/ship/v1/destroyer_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook_test.go @@ -19,16 +19,18 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Destroyer Webhook", func() { var ( - obj *Destroyer + obj = &shipv1.Destroyer{} ) BeforeEach(func() { - obj = &Destroyer{} + obj = &shipv1.Destroyer{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests diff --git a/testdata/project-v4-multigroup/internal/webhook/ship/v1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/ship/v1/webhook_suite_test.go new file mode 100644 index 00000000000..251f78b63fe --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v1/webhook_suite_test.go @@ -0,0 +1,150 @@ +/* +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" + + shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/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 = shipv1.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 = SetupDestroyerWebhookWithManager(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-multigroup/api/ship/v1beta1/frigate_webhook.go b/testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook.go similarity index 74% rename from testdata/project-v4-multigroup/api/ship/v1beta1/frigate_webhook.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook.go index c699e518551..90b5342fa05 100644 --- a/testdata/project-v4-multigroup/api/ship/v1beta1/frigate_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook.go @@ -19,16 +19,17 @@ package v1beta1 import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + + shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1" ) // nolint:unused // log is for logging in this package. var frigatelog = logf.Log.WithName("frigate-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Frigate) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupFrigateWebhookWithManager registers the webhook for Frigate in the manager. +func SetupFrigateWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&shipv1beta1.Frigate{}). Complete() } diff --git a/testdata/project-v4-multigroup/api/ship/v1beta1/frigate_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook_test.go similarity index 87% rename from testdata/project-v4-multigroup/api/ship/v1beta1/frigate_webhook_test.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook_test.go index ceeae183858..8fb1d99f493 100644 --- a/testdata/project-v4-multigroup/api/ship/v1beta1/frigate_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v1beta1/frigate_webhook_test.go @@ -19,16 +19,18 @@ package v1beta1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Frigate Webhook", func() { var ( - obj *Frigate + obj = &shipv1beta1.Frigate{} ) BeforeEach(func() { - obj = &Frigate{} + obj = &shipv1beta1.Frigate{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -42,7 +44,7 @@ var _ = Describe("Frigate Webhook", func() { // TODO (user): Add logic to convert the object to the desired version and verify the conversion // Example: // It("Should convert the object correctly", func() { - // convertedObj := &Frigate{} + // convertedObj := &shipv1beta1.Frigate{} // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) // Expect(convertedObj).ToNot(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook.go b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go similarity index 87% rename from testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go index 28c1fb1b72b..351d81576e3 100644 --- a/testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go @@ -25,16 +25,17 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1" ) // nolint:unused // log is for logging in this package. var cruiserlog = logf.Log.WithName("cruiser-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Cruiser) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCruiserWebhookWithManager registers the webhook for Cruiser in the manager. +func SetupCruiserWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&shipv2alpha1.Cruiser{}). WithValidator(&CruiserCustomValidator{}). Complete() } @@ -60,7 +61,7 @@ var _ webhook.CustomValidator = &CruiserCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Cruiser. func (v *CruiserCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cruiser, ok := obj.(*Cruiser) + cruiser, ok := obj.(*shipv2alpha1.Cruiser) if !ok { return nil, fmt.Errorf("expected a Cruiser object but got %T", obj) } @@ -73,9 +74,9 @@ func (v *CruiserCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Cruiser. func (v *CruiserCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cruiser, ok := newObj.(*Cruiser) + cruiser, ok := newObj.(*shipv2alpha1.Cruiser) if !ok { - return nil, fmt.Errorf("expected a Cruiser object but got %T", newObj) + return nil, fmt.Errorf("expected a Cruiser object for the newObj but got %T", newObj) } cruiserlog.Info("Validation for Cruiser upon update", "name", cruiser.GetName()) @@ -86,7 +87,7 @@ func (v *CruiserCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Cruiser. func (v *CruiserCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cruiser, ok := obj.(*Cruiser) + cruiser, ok := obj.(*shipv2alpha1.Cruiser) if !ok { return nil, fmt.Errorf("expected a Cruiser object but got %T", obj) } diff --git a/testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook_test.go b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook_test.go similarity index 89% rename from testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook_test.go rename to testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook_test.go index e548fad5f57..e66e3622b22 100644 --- a/testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_webhook_test.go +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook_test.go @@ -19,16 +19,18 @@ package v2alpha1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Cruiser Webhook", func() { var ( - obj *Cruiser + obj = &shipv2alpha1.Cruiser{} ) BeforeEach(func() { - obj = &Cruiser{} + obj = &shipv2alpha1.Cruiser{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -55,7 +57,7 @@ var _ = Describe("Cruiser Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &shipv2alpha1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/webhook_suite_test.go b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/webhook_suite_test.go new file mode 100644 index 00000000000..22fe9423793 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/webhook_suite_test.go @@ -0,0 +1,150 @@ +/* +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 v2alpha1 + +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" + + shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1" + + // +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 = shipv2alpha1.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 = SetupCruiserWebhookWithManager(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-with-plugins/Dockerfile b/testdata/project-v4-with-plugins/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/testdata/project-v4-with-plugins/Dockerfile +++ b/testdata/project-v4-with-plugins/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/testdata/project-v4-with-plugins/api/v1alpha1/webhook_suite_test.go b/testdata/project-v4-with-plugins/api/v1alpha1/webhook_suite_test.go deleted file mode 100644 index e70fab04bb0..00000000000 --- a/testdata/project-v4-with-plugins/api/v1alpha1/webhook_suite_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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 v1alpha1 - -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" - // +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 = 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 = (&Memcached{}).SetupWebhookWithManager(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-with-plugins/api/v1alpha1/zz_generated.deepcopy.go b/testdata/project-v4-with-plugins/api/v1alpha1/zz_generated.deepcopy.go index a41c7b842d1..6254bdb0507 100644 --- a/testdata/project-v4-with-plugins/api/v1alpha1/zz_generated.deepcopy.go +++ b/testdata/project-v4-with-plugins/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4-with-plugins/cmd/main.go b/testdata/project-v4-with-plugins/cmd/main.go index ade191db8f1..ed32294e2be 100644 --- a/testdata/project-v4-with-plugins/cmd/main.go +++ b/testdata/project-v4-with-plugins/cmd/main.go @@ -37,6 +37,7 @@ import ( examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1" "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/internal/controller" + webhookexamplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/internal/webhook/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -162,7 +163,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&examplecomv1alpha1.Memcached{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookexamplecomv1alpha1.SetupMemcachedWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Memcached") os.Exit(1) } diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go similarity index 86% rename from testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go rename to testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go index 11f098e7358..2ba002a4d65 100644 --- a/testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_webhook.go +++ b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go @@ -25,16 +25,17 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1" ) // nolint:unused // log is for logging in this package. var memcachedlog = logf.Log.WithName("memcached-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupMemcachedWebhookWithManager registers the webhook for Memcached in the manager. +func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&examplecomv1alpha1.Memcached{}). WithValidator(&MemcachedCustomValidator{}). Complete() } @@ -60,7 +61,7 @@ var _ webhook.CustomValidator = &MemcachedCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - memcached, ok := obj.(*Memcached) + memcached, ok := obj.(*examplecomv1alpha1.Memcached) if !ok { return nil, fmt.Errorf("expected a Memcached object but got %T", obj) } @@ -73,9 +74,9 @@ func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runti // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - memcached, ok := newObj.(*Memcached) + memcached, ok := newObj.(*examplecomv1alpha1.Memcached) if !ok { - return nil, fmt.Errorf("expected a Memcached object but got %T", newObj) + return nil, fmt.Errorf("expected a Memcached object for the newObj but got %T", newObj) } memcachedlog.Info("Validation for Memcached upon update", "name", memcached.GetName()) @@ -86,7 +87,7 @@ func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, n // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - memcached, ok := obj.(*Memcached) + memcached, ok := obj.(*examplecomv1alpha1.Memcached) if !ok { return nil, fmt.Errorf("expected a Memcached object but got %T", obj) } diff --git a/testdata/project-v4-with-plugins/api/v1alpha1/memcached_webhook_test.go b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook_test.go similarity index 88% rename from testdata/project-v4-with-plugins/api/v1alpha1/memcached_webhook_test.go rename to testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook_test.go index b966fb2d8da..b3fbc3a83a9 100644 --- a/testdata/project-v4-with-plugins/api/v1alpha1/memcached_webhook_test.go +++ b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook_test.go @@ -19,16 +19,18 @@ package v1alpha1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Memcached Webhook", func() { var ( - obj *Memcached + obj = &examplecomv1alpha1.Memcached{} ) BeforeEach(func() { - obj = &Memcached{} + obj = &examplecomv1alpha1.Memcached{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -55,7 +57,7 @@ var _ = Describe("Memcached Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &examplecomv1alpha1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/webhook_suite_test.go similarity index 95% rename from testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go rename to testdata/project-v4-with-plugins/internal/webhook/v1alpha1/webhook_suite_test.go index 94caeb96601..7e2fed6c3ae 100644 --- a/testdata/project-v4-multigroup/api/example.com/v1alpha1/webhook_suite_test.go +++ b/testdata/project-v4-with-plugins/internal/webhook/v1alpha1/webhook_suite_test.go @@ -30,6 +30,9 @@ import ( . "github.com/onsi/gomega" admissionv1 "k8s.io/api/admission/v1" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1" + // +kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" @@ -89,7 +92,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() - err = AddToScheme(scheme) + err = examplecomv1alpha1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1.AddToScheme(scheme) @@ -115,7 +118,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Memcached{}).SetupWebhookWithManager(mgr) + err = SetupMemcachedWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook diff --git a/testdata/project-v4/Dockerfile b/testdata/project-v4/Dockerfile index a48973ee7f3..4ba18b68cc4 100644 --- a/testdata/project-v4/Dockerfile +++ b/testdata/project-v4/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/testdata/project-v4/api/v1/webhook_suite_test.go b/testdata/project-v4/api/v1/webhook_suite_test.go deleted file mode 100644 index 418ca3f9291..00000000000 --- a/testdata/project-v4/api/v1/webhook_suite_test.go +++ /dev/null @@ -1,150 +0,0 @@ -/* -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" - // +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 = 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 = (&Captain{}).SetupWebhookWithManager(mgr) - Expect(err).NotTo(HaveOccurred()) - - err = (&Admiral{}).SetupWebhookWithManager(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/api/v1/zz_generated.deepcopy.go b/testdata/project-v4/api/v1/zz_generated.deepcopy.go index 4ec350e23aa..24fb3a25515 100644 --- a/testdata/project-v4/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v4/api/v1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v4/cmd/main.go b/testdata/project-v4/cmd/main.go index d2a65954c40..fb8995bc703 100644 --- a/testdata/project-v4/cmd/main.go +++ b/testdata/project-v4/cmd/main.go @@ -37,6 +37,7 @@ import ( crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/controller" + webhookcrewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1" // +kubebuilder:scaffold:imports ) @@ -153,7 +154,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&crewv1.Captain{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookcrewv1.SetupCaptainWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Captain") os.Exit(1) } @@ -167,7 +168,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&crewv1.FirstMate{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookcrewv1.SetupFirstMateWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "FirstMate") os.Exit(1) } @@ -181,7 +182,7 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&crewv1.Admiral{}).SetupWebhookWithManager(mgr); err != nil { + if err = webhookcrewv1.SetupAdmiralWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Admiral") os.Exit(1) } diff --git a/testdata/project-v4/api/v1/admiral_webhook.go b/testdata/project-v4/internal/webhook/v1/admiral_webhook.go similarity index 87% rename from testdata/project-v4/api/v1/admiral_webhook.go rename to testdata/project-v4/internal/webhook/v1/admiral_webhook.go index feff9708a4b..8c51732121e 100644 --- a/testdata/project-v4/api/v1/admiral_webhook.go +++ b/testdata/project-v4/internal/webhook/v1/admiral_webhook.go @@ -24,16 +24,17 @@ import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" ) // nolint:unused // log is for logging in this package. var admirallog = logf.Log.WithName("admiral-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Admiral) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupAdmiralWebhookWithManager registers the webhook for Admiral in the manager. +func SetupAdmiralWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.Admiral{}). WithDefaulter(&AdmiralCustomDefaulter{}). Complete() } @@ -56,7 +57,8 @@ var _ webhook.CustomDefaulter = &AdmiralCustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Admiral. func (d *AdmiralCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - admiral, ok := obj.(*Admiral) + admiral, ok := obj.(*crewv1.Admiral) + if !ok { return fmt.Errorf("expected an Admiral object but got %T", obj) } diff --git a/testdata/project-v4/api/v1/admiral_webhook_test.go b/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go similarity index 92% rename from testdata/project-v4/api/v1/admiral_webhook_test.go rename to testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go index 01cd6c5e141..2ffcbfd2498 100644 --- a/testdata/project-v4/api/v1/admiral_webhook_test.go +++ b/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go @@ -19,16 +19,18 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Admiral Webhook", func() { var ( - obj *Admiral + obj = &crewv1.Admiral{} ) BeforeEach(func() { - obj = &Admiral{} + obj = &crewv1.Admiral{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests diff --git a/testdata/project-v4/api/v1/captain_webhook.go b/testdata/project-v4/internal/webhook/v1/captain_webhook.go similarity index 90% rename from testdata/project-v4/api/v1/captain_webhook.go rename to testdata/project-v4/internal/webhook/v1/captain_webhook.go index 98fc273afc7..3145ec12572 100644 --- a/testdata/project-v4/api/v1/captain_webhook.go +++ b/testdata/project-v4/internal/webhook/v1/captain_webhook.go @@ -25,16 +25,17 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" ) // nolint:unused // log is for logging in this package. var captainlog = logf.Log.WithName("captain-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *Captain) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupCaptainWebhookWithManager registers the webhook for Captain in the manager. +func SetupCaptainWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.Captain{}). WithValidator(&CaptainCustomValidator{}). WithDefaulter(&CaptainCustomDefaulter{}). Complete() @@ -58,7 +59,8 @@ var _ webhook.CustomDefaulter = &CaptainCustomDefaulter{} // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Captain. func (d *CaptainCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) + if !ok { return fmt.Errorf("expected an Captain object but got %T", obj) } @@ -88,7 +90,7 @@ var _ webhook.CustomValidator = &CaptainCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) if !ok { return nil, fmt.Errorf("expected a Captain object but got %T", obj) } @@ -101,9 +103,9 @@ func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - captain, ok := newObj.(*Captain) + captain, ok := newObj.(*crewv1.Captain) if !ok { - return nil, fmt.Errorf("expected a Captain object but got %T", newObj) + return nil, fmt.Errorf("expected a Captain object for the newObj but got %T", newObj) } captainlog.Info("Validation for Captain upon update", "name", captain.GetName()) @@ -114,7 +116,7 @@ func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - captain, ok := obj.(*Captain) + captain, ok := obj.(*crewv1.Captain) if !ok { return nil, fmt.Errorf("expected a Captain object but got %T", obj) } diff --git a/testdata/project-v4-multigroup/api/crew/v1/captain_webhook_test.go b/testdata/project-v4/internal/webhook/v1/captain_webhook_test.go similarity index 92% rename from testdata/project-v4-multigroup/api/crew/v1/captain_webhook_test.go rename to testdata/project-v4/internal/webhook/v1/captain_webhook_test.go index 4c1020c9c56..73435820ee4 100644 --- a/testdata/project-v4-multigroup/api/crew/v1/captain_webhook_test.go +++ b/testdata/project-v4/internal/webhook/v1/captain_webhook_test.go @@ -19,16 +19,18 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("Captain Webhook", func() { var ( - obj *Captain + obj = &crewv1.Captain{} ) BeforeEach(func() { - obj = &Captain{} + obj = &crewv1.Captain{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -66,7 +68,7 @@ var _ = Describe("Captain Webhook", func() { // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj := &crewv1.Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) // }) diff --git a/testdata/project-v4/api/v1/firstmate_webhook.go b/testdata/project-v4/internal/webhook/v1/firstmate_webhook.go similarity index 76% rename from testdata/project-v4/api/v1/firstmate_webhook.go rename to testdata/project-v4/internal/webhook/v1/firstmate_webhook.go index e19ae07ada5..8b009e57c05 100644 --- a/testdata/project-v4/api/v1/firstmate_webhook.go +++ b/testdata/project-v4/internal/webhook/v1/firstmate_webhook.go @@ -19,16 +19,17 @@ package v1 import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" ) // nolint:unused // log is for logging in this package. var firstmatelog = logf.Log.WithName("firstmate-resource") -// SetupWebhookWithManager will setup the manager to manage the webhooks. -func (r *FirstMate) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). +// SetupFirstMateWebhookWithManager registers the webhook for FirstMate in the manager. +func SetupFirstMateWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&crewv1.FirstMate{}). Complete() } diff --git a/testdata/project-v4/api/v1/firstmate_webhook_test.go b/testdata/project-v4/internal/webhook/v1/firstmate_webhook_test.go similarity index 89% rename from testdata/project-v4/api/v1/firstmate_webhook_test.go rename to testdata/project-v4/internal/webhook/v1/firstmate_webhook_test.go index 040e5dc3ee6..cc9fdb97fcc 100644 --- a/testdata/project-v4/api/v1/firstmate_webhook_test.go +++ b/testdata/project-v4/internal/webhook/v1/firstmate_webhook_test.go @@ -19,16 +19,18 @@ package v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" // TODO (user): Add any additional imports if needed ) var _ = Describe("FirstMate Webhook", func() { var ( - obj *FirstMate + obj = &crewv1.FirstMate{} ) BeforeEach(func() { - obj = &FirstMate{} + obj = &crewv1.FirstMate{} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") // TODO (user): Add any setup logic common to all tests @@ -42,7 +44,7 @@ var _ = Describe("FirstMate Webhook", func() { // TODO (user): Add logic to convert the object to the desired version and verify the conversion // Example: // It("Should convert the object correctly", func() { - // convertedObj := &FirstMate{} + // convertedObj := &crewv1.FirstMate{} // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) // Expect(convertedObj).ToNot(BeNil()) // }) diff --git a/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go new file mode 100644 index 00000000000..c49251cd192 --- /dev/null +++ b/testdata/project-v4/internal/webhook/v1/webhook_suite_test.go @@ -0,0 +1,153 @@ +/* +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" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/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 = crewv1.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 = SetupCaptainWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = SetupAdmiralWebhookWithManager(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()) +})