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..cecc61a6ff8 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 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..2d1e194f95b 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 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..5bfc38a1da0 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 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..b1375ebc261 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 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 77% 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..1918e6a51f5 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,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := apimachineryruntime.NewScheme() - err = AddToScheme(scheme) + err = %s.AddToScheme(scheme) 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..435b01a15e0 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", "%[version]", "%[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 89% 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..8cf9b359573 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 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..1b9a0d157fd 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 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..37a14fbd466 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 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..53b5a4c6770 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 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..f674c625d9e 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 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()) +})