diff --git a/.github/workflows/legacy-webhook-path.yml b/.github/workflows/legacy-webhook-path.yml new file mode 100644 index 00000000000..c094e23de76 --- /dev/null +++ b/.github/workflows/legacy-webhook-path.yml @@ -0,0 +1,32 @@ +# This test ensure that the legacy webhook path +# still working. The option is deprecated +# and should be removed when we no longer need +# to support go/v4 plugin. +name: Legacy Webhook Path + +on: + push: + paths: + - 'testdata/**' + - '.github/workflows/legacy-webhook-path.yml' + pull_request: + paths: + - 'testdata/**' + - '.github/workflows/legacy-webhook-path.yml' + +jobs: + webhook-legacy-path: + name: Verify Legacy Webhook Path + runs-on: ubuntu-latest + # Pull requests from the same repository won't trigger this checks as they were already triggered by the push + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Clone the code + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.3' + - name: Run make test-legacy + run: make test-legacy + diff --git a/.gitignore b/.gitignore index 3cd3d73e21f..ed4eb0868b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ docs/book/src/docs # skip testdata go.sum, since it may have # different result depending on go version /testdata/**/go.sum -/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/bin \ No newline at end of file +/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/bin +/testdata/**legacy** diff --git a/Makefile b/Makefile index be8113b411c..6d90ad0d036 100644 --- a/Makefile +++ b/Makefile @@ -164,3 +164,10 @@ test-license: ## Run the license check .PHONY: test-spaces test-spaces: ## Run the trailing spaces check ./test/check_spaces.sh + +## TODO: Remove me when go/v4 plugin be removed +## Deprecated +.PHONY: test-legacy +test-legacy: ## Run the tests to validate legacy path for webhooks + rm -rf ./testdata/**legacy**/ + ./test/testdata/legacy-webhook-path.sh 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 80% 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..0ae648962cf 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, @@ -72,7 +73,6 @@ This marker is responsible for generating a mutation webhook manifest. // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // @@ -81,7 +81,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 +98,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 } } @@ -154,7 +156,6 @@ This marker is responsible for generating a validation webhook manifest. */ // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // @@ -168,29 +169,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv1.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -205,12 +206,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 +221,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 +234,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 +263,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/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/cronjob_conversion.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go index 36485072ec8..10524383e34 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go @@ -17,9 +17,9 @@ package v1 /* Implementing the hub method is pretty easy -- we just have to add an empty -method called `Hub()` to serve as a +method called `Hub()`to serve as a [marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). -We could also just put this inline in our `cronjob_types.go` file. +We could also just put this inline in our cronjob_types.go file. */ // Hub marks this type as a conversion hub. 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/cronjob_conversion.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go index ac971d8264a..28fa9d6520b 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go @@ -35,8 +35,8 @@ import ( /* 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. +interface. Namely, they'll need `ConvertTo()` and `ConvertFrom()` +methods to convert to/from the hub version. */ /* 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_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..9c96522aa58 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, @@ -76,7 +77,6 @@ This marker is responsible for generating a mutation webhook manifest. // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // @@ -85,7 +85,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 +102,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 } } @@ -158,7 +160,6 @@ This marker is responsible for generating a validation webhook manifest. */ // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // @@ -172,29 +173,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv1.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv1.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -209,12 +210,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 +225,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 +238,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 +267,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_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..297e52f89d2 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, @@ -55,16 +57,14 @@ func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=mcronjob-v2.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as it is used only for temporary operations and does not need to be deeply copied. type CronJobCustomDefaulter struct { - // Default values for various CronJob fields - DefaultConcurrencyPolicy ConcurrencyPolicy + DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 @@ -74,16 +74,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. @@ -91,7 +92,6 @@ func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=vcronjob-v2.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // @@ -105,29 +105,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv2.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cronjob, ok := newObj.(*CronJob) + cronjob, ok := newObj.(*batchv2.CronJob) if !ok { - return nil, fmt.Errorf("expected a CronJob object but got %T", newObj) + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) } cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) - return nil, cronjob.validateCronJob() + return nil, validateCronJob(cronjob) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - cronjob, ok := obj.(*CronJob) + cronjob, ok := obj.(*batchv2.CronJob) if !ok { return nil, fmt.Errorf("expected a CronJob object but got %T", obj) } @@ -138,65 +138,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 69% 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..acd7ea139d8 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,18 +19,28 @@ 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{} + oldObj = &batchv2.CronJob{} + validator CronJobCustomValidator + defaulter CronJobCustomDefaulter ) BeforeEach(func() { - obj = &CronJob{} + obj = &batchv2.CronJob{} + oldObj = &batchv2.CronJob{} + validator = CronJobCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = CronJobCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -43,8 +53,11 @@ var _ = Describe("CronJob Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) @@ -55,20 +68,20 @@ var _ = Describe("CronJob Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // }) }) @@ -76,7 +89,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..3350b3258c2 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,37 @@ +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 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= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -28,11 +44,13 @@ 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/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= 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= @@ -45,27 +63,43 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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..c8873deb891 100644 --- a/hack/docs/internal/cronjob-tutorial/generate_cronjob.go +++ b/hack/docs/internal/cronjob-tutorial/generate_cronjob.go @@ -375,18 +375,9 @@ 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`, - ` - oldObj *CronJob - validator CronJobCustomValidator`) - hackutils.CheckError("insert global vars", err) - - err = pluginutil.ReplaceInFile(file, + err := pluginutil.ReplaceInFile(file, webhookTestCreateDefaultingFragment, webhookTestCreateDefaultingReplaceFragment) hackutils.CheckError("replace create defaulting test", err) @@ -399,36 +390,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 +428,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 +437,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 +469,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 +477,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..4b909e48dc3 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 @@ -188,8 +190,11 @@ const webhookTestCreateDefaultingFragment = `// TODO (user): Add logic for defau // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // })` @@ -201,10 +206,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 +217,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 +226,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") @@ -235,20 +240,20 @@ const webhookTestingValidatingTodoFragment = `// TODO (user): Add logic for vali // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // })` const webhookTestingValidatingExampleFragment = `It("Should deny creation if the name is too long", func() { @@ -303,15 +308,20 @@ 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{} + oldObj = &batchv1.CronJob{} + validator = CronJobCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = CronJobCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests` -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 +329,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 +341,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..79896073ca1 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 @@ -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..e22e4f698ae 100644 --- a/hack/docs/internal/multiversion-tutorial/hub.go +++ b/hack/docs/internal/multiversion-tutorial/hub.go @@ -36,13 +36,14 @@ package v1 /* Implementing the hub method is pretty easy -- we just have to add an empty -method called ` + "`Hub()`" + ` to serve as a +method called ` + "`" + `Hub()` + "`" + `to serve as a [marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). -We could also just put this inline in our ` + "`cronjob_types.go`" + ` file. +We could also just put this inline in our cronjob_types.go file. */ // Hub marks this type as a conversion hub. -func (*CronJob) Hub() {}` +func (*CronJob) Hub() {} +` const hubV2Code = `/* Licensed under the Apache License, Version 2.0 (the "License"); @@ -81,8 +82,8 @@ import ( /* 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. +interface. Namely, they'll need ` + "`" + `ConvertTo()` + "`" + ` and ` + "`" + `ConvertFrom()` + "`" + ` +methods to convert to/from the hub version. */ /* 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..e2a277ce367 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 { + importPath := fmt.Sprintf("webhook%s", f.Resource.ImportAlias()) + if !f.MultiGroup || f.Resource.Group == "" { + imports = append(imports, fmt.Sprintf(webhookImportCodeFragment, importPath, f.Repo, f.Resource.Version)) + } else { + imports = append(imports, fmt.Sprintf(multiGroupWebhookImportCodeFragment, importPath, + 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 79% 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..57656166184 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,28 @@ 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 == "" { + // Deprecated: Remove me when remove go/v4 + // nolint:goconst + baseDir := "api" + if !f.IsLegacyPath { + baseDir = filepath.Join("internal", "webhook") + } + if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook.go") + f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook.go") } else { - f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook.go") + f.Path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook.go") } } @@ -97,12 +110,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 +134,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! ` @@ -123,7 +160,9 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { defaultingWebhookTemplate = ` // +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +{{ if .IsLegacyPath -}} // +kubebuilder:object:generate=false +{{- end }} // {{ .Resource.Kind }}CustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind {{ .Resource.Kind }} when those are created or updated. // @@ -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) } @@ -156,7 +200,9 @@ func (d *{{ .Resource.Kind }}CustomDefaulter) Default(ctx context.Context, obj r // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }} +{{ if .IsLegacyPath -}} // +kubebuilder:object:generate=false +{{- end }} // {{ .Resource.Kind }}CustomValidator struct is responsible for validating the {{ .Resource.Kind }} resource // when it is created, updated, or deleted. // @@ -170,7 +216,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 +233,13 @@ func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(ctx context.Context // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}. func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + {{- if .IsLegacyPath -}} {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.Kind }}) + {{- else }} + {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + {{- end }} if !ok { - return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", newObj) + return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object for the newObj but got %T", newObj) } {{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon update", "name", {{ lower .Resource.Kind }}.GetName()) @@ -196,7 +250,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 52% 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..1db4f07a961 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,34 +44,66 @@ 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 == "" { + // Deprecated: Remove me when remove go/v4 + // nolint:goconst + baseDir := "api" + if !f.IsLegacyPath { + baseDir = filepath.Join("internal", "webhook") + } + if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "webhook_suite_test.go") + f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "webhook_suite_test.go") } else { - f.Path = filepath.Join("api", "%[version]", "webhook_suite_test.go") + f.Path = filepath.Join(baseDir, "%[version]", "webhook_suite_test.go") } } f.Path = f.Resource.Replacer().Replace(f.Path) log.Println(f.Path) - f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate, - machinery.NewMarkerFor(f.Path, importMarker), - 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 { + f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplateLegacy, + machinery.NewMarkerFor(f.Path, importMarker), + admissionImportAlias, + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), + "%s", + "%d", + ) + } else { + f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate, + machinery.NewMarkerFor(f.Path, importMarker), + f.Resource.ImportAlias(), admissionImportAlias, + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), + "%s", + "%d", + ) + } + + 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 +130,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 +149,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 +159,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 { @@ -178,6 +224,137 @@ func TestAPIs(t *testing.T) { 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({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")}, + ErrorIfCRDPathMissing: {{ .WireResource }}, + + // 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({{ .BaseDirectoryRelativePath }}, "bin", "k8s", + fmt.Sprintf("{{ .K8SVersion }}-%%s-%%s", runtime.GOOS, runtime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "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 = %s.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = %s.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + %s + + 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()) + + %s + + 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:%s", 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()) +}) +` + +const webhookTestSuiteTemplateLegacy = `{{ .Boilerplate }} + +package {{ .Resource.Version }} + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + "runtime" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + %s + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + 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" +) + +// 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))) 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 60% 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..eef6491f0ad 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,28 @@ 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 == "" { + // Deprecated: Remove me when remove go/v4 + // nolint:goconst + baseDir := "api" + if !f.IsLegacyPath { + baseDir = filepath.Join("internal", "webhook") + } + if f.MultiGroup && f.Resource.Group != "" { - f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_webhook_test.go") + f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook_test.go") } else { - f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook_test.go") + f.Path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook_test.go") } } f.Path = f.Resource.Replacer().Replace(f.Path) @@ -78,18 +91,47 @@ 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 ( + {{- if .IsLegacyPath -}} obj *{{ .Resource.Kind }} + {{- else }} + obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + oldObj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + {{- if .Resource.HasValidationWebhook }} + validator {{ .Resource.Kind }}CustomValidator + {{- end }} + {{- if .Resource.HasDefaultingWebhook }} + defaulter {{ .Resource.Kind }}CustomDefaulter + {{- end }} + {{- end }} ) BeforeEach(func() { + {{- if .IsLegacyPath -}} obj = &{{ .Resource.Kind }}{} + {{- else }} + obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + oldObj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + {{- if .Resource.HasValidationWebhook }} + validator = {{ .Resource.Kind }}CustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + {{- end }} + {{- if .Resource.HasDefaultingWebhook }} + defaulter = {{ .Resource.Kind }}CustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + {{- end }} + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + {{- end }} Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -106,7 +148,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()) // }) @@ -120,20 +166,34 @@ Context("When creating or updating {{ .Resource.Kind }} under Validating Webhook // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" + {{- if .IsLegacyPath -}} // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + {{- else }} + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + {{- end }} // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" + {{- if .IsLegacyPath -}} // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + {{- else }} + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + {{- end }} // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") + {{- if .IsLegacyPath -}} // oldObj := &Captain{SomeRequiredField: "valid_value"} // obj.SomeRequiredField = "updated_value" // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + {{- else }} + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + {{- end }} // }) }) ` @@ -144,9 +204,18 @@ Context("When creating {{ .Resource.Kind }} under Defaulting Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + {{- if .IsLegacyPath -}} // obj.SomeFieldWithDefault = "" // Expect(obj.Default(ctx)).To(Succeed()) // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + {{- else }} + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + {{- end }} // }) }) ` 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/test/testdata/legacy-webhook-path.sh b/test/testdata/legacy-webhook-path.sh new file mode 100755 index 00000000000..e702d9d2791 --- /dev/null +++ b/test/testdata/legacy-webhook-path.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +# 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. + +############################## +# TODO: Remove me when go/v4 is no longer supported +# This script i used to validate the legacy webhook path +############################## + +source "$(dirname "$0")/../common.sh" + +# This function scaffolds test projects given a project name and flags. +# +# Usage: +# +# scaffold_test_project +function scaffold_test_project { + local project=$1 + shift + local init_flags="$@" + + local testdata_dir="$(dirname "$0")/../../testdata" + mkdir -p $testdata_dir/$project + rm -rf $testdata_dir/$project/* + pushd $testdata_dir/$project + + header_text "Generating project ${project} with flags: ${init_flags}" + go mod init sigs.k8s.io/kubebuilder/testdata/$project # our repo autodetection will traverse up to the kb module if we don't do this + header_text "Initializing project ..." + $kb init $init_flags --domain testproject.org --license apache2 --owner "The Kubernetes authors" + + if [ $project == "legacy-project-v4" ] ; then + header_text 'Creating APIs ...' + $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false + $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false --force + $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --legacy=true + $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + $kb create webhook --group crew --version v1 --kind FirstMate --conversion --legacy=true + $kb create api --group crew --version v1 --kind Admiral --plural=admirales --controller=true --resource=true --namespaced=false --make=false + $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting --legacy=true + fi + + if [[ $project =~ multigroup ]]; then + header_text 'Switching to multigroup layout ...' + $kb edit --multigroup=true + + header_text 'Creating APIs ...' + $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false + $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --legacy=true + + $kb create api --group ship --version v1beta1 --kind Frigate --controller=true --resource=true --make=false + $kb create webhook --group ship --version v1beta1 --kind Frigate --conversion --legacy=true + $kb create api --group ship --version v1 --kind Destroyer --controller=true --resource=true --namespaced=false --make=false + $kb create webhook --group ship --version v1 --kind Destroyer --defaulting --legacy=true + $kb create api --group ship --version v2alpha1 --kind Cruiser --controller=true --resource=true --namespaced=false --make=false + $kb create webhook --group ship --version v2alpha1 --kind Cruiser --programmatic-validation --legacy=true + + $kb create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true --make=false + $kb create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true --make=false + $kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false + $kb create api --group apps --version v1 --kind Deployment --controller=true --resource=false --make=false + $kb create api --group foo --version v1 --kind Bar --controller=true --resource=true --make=false + $kb create api --group fiz --version v1 --kind Bar --controller=true --resource=true --make=false + fi + + if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then + header_text 'With Optional Plugins ...' + header_text 'Creating APIs with deploy-image plugin ...' + $kb create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false + $kb create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.36.1 --plugins="deploy-image/v1-alpha" --make=false + $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation --legacy=true + header_text 'Editing project with Grafana plugin ...' + $kb edit --plugins=grafana.kubebuilder.io/v1-alpha + fi + + make all + make build-installer + go mod tidy + make test + popd +} + +build_kb + +scaffold_test_project legacy-project-v4 --plugins="go/v4" +scaffold_test_project legacy-project-v4-multigroup --plugins="go/v4" +scaffold_test_project legacy-project-v4-with-plugins --plugins="go/v4" 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..5ff17553be1 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() @@ -44,7 +45,6 @@ func (r *Captain) SetupWebhookWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-captain,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=mcaptain-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CaptainCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind Captain when those are created or updated. // @@ -58,7 +58,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) } @@ -74,7 +75,6 @@ func (d *CaptainCustomDefaulter) Default(ctx context.Context, obj runtime.Object // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CaptainCustomValidator struct is responsible for validating the Captain resource // when it is created, updated, or deleted. // @@ -88,7 +88,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 +101,9 @@ func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - captain, ok := newObj.(*Captain) + captain, ok := newObj.(*crewv1.Captain) if !ok { - return nil, fmt.Errorf("expected a Captain object but got %T", newObj) + return nil, fmt.Errorf("expected a Captain object for the newObj but got %T", newObj) } captainlog.Info("Validation for Captain upon update", "name", captain.GetName()) @@ -114,7 +114,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 66% 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..af7e81a27a8 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,18 +19,28 @@ 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{} + oldObj = &crewv1.Captain{} + validator CaptainCustomValidator + defaulter CaptainCustomDefaulter ) BeforeEach(func() { - obj = &Captain{} + obj = &crewv1.Captain{} + oldObj = &crewv1.Captain{} + validator = CaptainCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = CaptainCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -43,8 +53,11 @@ var _ = Describe("Captain Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) @@ -55,20 +68,20 @@ var _ = Describe("Captain Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).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..e004affa05f 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() } @@ -46,7 +47,6 @@ func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // MemcachedCustomValidator struct is responsible for validating the Memcached resource // when it is created, updated, or deleted. // @@ -60,7 +60,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 +73,9 @@ func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runti // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - memcached, ok := newObj.(*Memcached) + memcached, ok := newObj.(*examplecomv1alpha1.Memcached) if !ok { - return nil, fmt.Errorf("expected a Memcached object but got %T", newObj) + return nil, fmt.Errorf("expected a Memcached object for the newObj but got %T", newObj) } memcachedlog.Info("Validation for Memcached upon update", "name", memcached.GetName()) @@ -86,7 +86,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 68% 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..055bb4d7006 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,18 +19,25 @@ 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{} + oldObj = &examplecomv1alpha1.Memcached{} + validator MemcachedCustomValidator ) BeforeEach(func() { - obj = &Memcached{} + obj = &examplecomv1alpha1.Memcached{} + oldObj = &examplecomv1alpha1.Memcached{} + validator = MemcachedCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -44,20 +51,20 @@ var _ = Describe("Memcached Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).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 86% 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..37711b210f0 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() } @@ -42,7 +43,6 @@ func (r *Destroyer) SetupWebhookWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-ship-testproject-org-v1-destroyer,mutating=true,failurePolicy=fail,sideEffects=None,groups=ship.testproject.org,resources=destroyers,verbs=create;update,versions=v1,name=mdestroyer-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // DestroyerCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind Destroyer when those are created or updated. // @@ -56,7 +56,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 68% 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..87f30307302 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,18 +19,25 @@ 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{} + oldObj = &shipv1.Destroyer{} + defaulter DestroyerCustomDefaulter ) BeforeEach(func() { - obj = &Destroyer{} + obj = &shipv1.Destroyer{} + oldObj = &shipv1.Destroyer{} + defaulter = DestroyerCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -43,8 +50,11 @@ var _ = Describe("Destroyer Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) diff --git a/testdata/project-v4-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 80% 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..a7e675e779e 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,18 +19,22 @@ 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{} + oldObj = &shipv1beta1.Frigate{} ) BeforeEach(func() { - obj = &Frigate{} + obj = &shipv1beta1.Frigate{} + oldObj = &shipv1beta1.Frigate{} + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -42,7 +46,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..8637e993b4c 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() } @@ -46,7 +47,6 @@ func (r *Cruiser) SetupWebhookWithManager(mgr ctrl.Manager) error { // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-ship-testproject-org-v2alpha1-cruiser,mutating=false,failurePolicy=fail,sideEffects=None,groups=ship.testproject.org,resources=cruisers,verbs=create;update,versions=v2alpha1,name=vcruiser-v2alpha1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CruiserCustomValidator struct is responsible for validating the Cruiser resource // when it is created, updated, or deleted. // @@ -60,7 +60,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 +73,9 @@ func (v *CruiserCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Cruiser. func (v *CruiserCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - cruiser, ok := newObj.(*Cruiser) + cruiser, ok := newObj.(*shipv2alpha1.Cruiser) if !ok { - return nil, fmt.Errorf("expected a Cruiser object but got %T", newObj) + return nil, fmt.Errorf("expected a Cruiser object for the newObj but got %T", newObj) } cruiserlog.Info("Validation for Cruiser upon update", "name", cruiser.GetName()) @@ -86,7 +86,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 70% 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..049a8a842ed 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,18 +19,25 @@ 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{} + oldObj = &shipv2alpha1.Cruiser{} + validator CruiserCustomValidator ) BeforeEach(func() { - obj = &Cruiser{} + obj = &shipv2alpha1.Cruiser{} + oldObj = &shipv2alpha1.Cruiser{} + validator = CruiserCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -44,20 +51,20 @@ var _ = Describe("Cruiser Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).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..496877aaf71 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() } @@ -46,7 +47,6 @@ func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // MemcachedCustomValidator struct is responsible for validating the Memcached resource // when it is created, updated, or deleted. // @@ -60,7 +60,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 +73,9 @@ func (v *MemcachedCustomValidator) ValidateCreate(ctx context.Context, obj runti // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached. func (v *MemcachedCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - memcached, ok := newObj.(*Memcached) + memcached, ok := newObj.(*examplecomv1alpha1.Memcached) if !ok { - return nil, fmt.Errorf("expected a Memcached object but got %T", newObj) + return nil, fmt.Errorf("expected a Memcached object for the newObj but got %T", newObj) } memcachedlog.Info("Validation for Memcached upon update", "name", memcached.GetName()) @@ -86,7 +86,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 69% 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..8c4afa64420 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,18 +19,25 @@ 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{} + oldObj = &examplecomv1alpha1.Memcached{} + validator MemcachedCustomValidator ) BeforeEach(func() { - obj = &Memcached{} + obj = &examplecomv1alpha1.Memcached{} + oldObj = &examplecomv1alpha1.Memcached{} + validator = MemcachedCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -44,20 +51,20 @@ var _ = Describe("Memcached Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).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..c4b9086f4ef 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() } @@ -42,7 +43,6 @@ func (r *Admiral) SetupWebhookWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-admiral,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=admirales,verbs=create;update,versions=v1,name=madmiral-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // AdmiralCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind Admiral when those are created or updated. // @@ -56,7 +56,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 69% 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..4b577a55ce3 100644 --- a/testdata/project-v4/api/v1/admiral_webhook_test.go +++ b/testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go @@ -19,18 +19,25 @@ 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{} + oldObj = &crewv1.Admiral{} + defaulter AdmiralCustomDefaulter ) BeforeEach(func() { - obj = &Admiral{} + obj = &crewv1.Admiral{} + oldObj = &crewv1.Admiral{} + defaulter = AdmiralCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -43,8 +50,11 @@ var _ = Describe("Admiral Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) diff --git a/testdata/project-v4/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..aaf0124bb0a 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() @@ -44,7 +45,6 @@ func (r *Captain) SetupWebhookWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-captain,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=mcaptain-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CaptainCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind Captain when those are created or updated. // @@ -58,7 +58,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) } @@ -74,7 +75,6 @@ func (d *CaptainCustomDefaulter) Default(ctx context.Context, obj runtime.Object // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. // +kubebuilder:webhook:path=/validate-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1 -// +kubebuilder:object:generate=false // CaptainCustomValidator struct is responsible for validating the Captain resource // when it is created, updated, or deleted. // @@ -88,7 +88,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 +101,9 @@ func (v *CaptainCustomValidator) ValidateCreate(ctx context.Context, obj runtime // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain. func (v *CaptainCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - captain, ok := newObj.(*Captain) + captain, ok := newObj.(*crewv1.Captain) if !ok { - return nil, fmt.Errorf("expected a Captain object but got %T", newObj) + return nil, fmt.Errorf("expected a Captain object for the newObj but got %T", newObj) } captainlog.Info("Validation for Captain upon update", "name", captain.GetName()) @@ -114,7 +114,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 66% 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..095f8b19c3f 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,18 +19,28 @@ 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{} + oldObj = &crewv1.Captain{} + validator CaptainCustomValidator + defaulter CaptainCustomDefaulter ) BeforeEach(func() { - obj = &Captain{} + obj = &crewv1.Captain{} + oldObj = &crewv1.Captain{} + validator = CaptainCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = CaptainCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -43,8 +53,11 @@ var _ = Describe("Captain Webhook", func() { // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") + // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" - // Expect(obj.Default(ctx)).To(Succeed()) + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) @@ -55,20 +68,20 @@ var _ = Describe("Captain Webhook", func() { // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" - // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred()) + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" - // Expect(obj.ValidateCreate(ctx)).To(BeNil()) + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") - // oldObj := &Captain{SomeRequiredField: "valid_value"} + // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" - // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil()) + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).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 82% 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..44b9b76808f 100644 --- a/testdata/project-v4/api/v1/firstmate_webhook_test.go +++ b/testdata/project-v4/internal/webhook/v1/firstmate_webhook_test.go @@ -19,18 +19,22 @@ 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{} + oldObj = &crewv1.FirstMate{} ) BeforeEach(func() { - obj = &FirstMate{} + obj = &crewv1.FirstMate{} + oldObj = &crewv1.FirstMate{} + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests }) @@ -42,7 +46,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()) +})