From 483d92dad003fd4882ba4f702d8d6e5297f9dfe0 Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Sat, 2 Nov 2024 18:32:41 +0000 Subject: [PATCH] (go/v4): Add hub and spoke implementation for conversion webhook Currently, the implementation is incomplete. It introduces the code implementation to generate the hub and spoke --- .../src/multiversion-tutorial/conversion.md | 12 +- .../testdata/project/PROJECT | 5 +- .../project/api/v1/cronjob_conversion.go | 2 + .../testdata/project/api/v1/cronjob_types.go | 2 + .../project/api/v2/cronjob_conversion.go | 24 +- .../internal/webhook/v1/cronjob_webhook.go | 1 + .../webhook/v1/cronjob_webhook_test.go | 10 + .../webhook/v2/cronjob_webhook_test.go | 10 - .../src/multiversion-tutorial/webhooks.md | 9 - docs/book/src/reference/project-config.md | 1 + .../generate_multiversion.go | 293 ++++++++++++++++-- .../internal/multiversion-tutorial/hub.go | 87 +----- pkg/cli/alpha/internal/generate.go | 9 +- pkg/model/resource/resource_test.go | 1 + pkg/model/resource/webhooks.go | 25 ++ pkg/model/resource/webhooks_test.go | 2 + .../common/kustomize/v2/scaffolds/webhook.go | 3 +- pkg/plugins/golang/options.go | 4 + pkg/plugins/golang/options_test.go | 1 + .../scaffolds/internal/templates/api/hub.go | 72 +++++ .../scaffolds/internal/templates/api/spoke.go | 94 ++++++ .../internal/templates/webhooks/webhook.go | 2 +- pkg/plugins/golang/v4/scaffolds/webhook.go | 36 +++ pkg/plugins/golang/v4/webhook.go | 48 ++- test/e2e/alphagenerate/generate_test.go | 13 + test/e2e/v4/generate_test.go | 65 +--- test/testdata/generate.sh | 29 +- test/testdata/legacy-webhook-path.sh | 7 +- testdata/project-v4-multigroup/PROJECT | 3 + .../example.com/v1/wordpress_conversion.go | 22 ++ .../api/example.com/v1/wordpress_types.go | 2 +- .../example.com/v2/wordpress_conversion.go | 45 +++ testdata/project-v4-with-plugins/PROJECT | 3 + .../api/v1/wordpress_conversion.go | 22 ++ .../api/v1/wordpress_types.go | 2 +- .../api/v2/wordpress_conversion.go | 45 +++ testdata/project-v4/PROJECT | 3 + .../project-v4/api/v1/firstmate_conversion.go | 22 ++ testdata/project-v4/api/v1/firstmate_types.go | 2 +- .../project-v4/api/v2/firstmate_conversion.go | 45 +++ 40 files changed, 840 insertions(+), 243 deletions(-) create mode 100644 pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go create mode 100644 pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v1/wordpress_conversion.go create mode 100644 testdata/project-v4-multigroup/api/example.com/v2/wordpress_conversion.go create mode 100644 testdata/project-v4-with-plugins/api/v1/wordpress_conversion.go create mode 100644 testdata/project-v4-with-plugins/api/v2/wordpress_conversion.go create mode 100644 testdata/project-v4/api/v1/firstmate_conversion.go create mode 100644 testdata/project-v4/api/v2/firstmate_conversion.go diff --git a/docs/book/src/multiversion-tutorial/conversion.md b/docs/book/src/multiversion-tutorial/conversion.md index 443b2a79284..39b37a907b2 100644 --- a/docs/book/src/multiversion-tutorial/conversion.md +++ b/docs/book/src/multiversion-tutorial/conversion.md @@ -1,8 +1,16 @@ # Implementing conversion With our model for conversion in place, it's time to actually implement -the conversion functions. We'll put them in a file called -`cronjob_conversion.go` next to our `cronjob_types.go` file, to avoid +the conversion functions. We'll create a conversion webhook +for our CronJob API version `v1` (Hub) to Spoke our CronJob API version +`v2` see: + +```go +kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2 +``` + +The above command will generate the `cronjob_conversion.go` next to our +`cronjob_types.go` file, to avoid cluttering up our main types file with extra functions. ## Hub... diff --git a/docs/book/src/multiversion-tutorial/testdata/project/PROJECT b/docs/book/src/multiversion-tutorial/testdata/project/PROJECT index 867776e2af8..084ae740bff 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/PROJECT +++ b/docs/book/src/multiversion-tutorial/testdata/project/PROJECT @@ -18,7 +18,11 @@ resources: path: tutorial.kubebuilder.io/project/api/v1 version: v1 webhooks: + conversion: true defaulting: true + spoke: + - v2 + - v2 validation: true webhookVersion: v1 - api: @@ -30,7 +34,6 @@ resources: path: tutorial.kubebuilder.io/project/api/v2 version: v2 webhooks: - conversion: true defaulting: true validation: true webhookVersion: v1 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 10524383e34..e7747661252 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 @@ -1,4 +1,6 @@ /* +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 diff --git a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_types.go b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_types.go index 8f3e38935bf..4c61fc2d485 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_types.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_types.go @@ -127,6 +127,8 @@ type CronJobStatus struct { */ // +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:conversion:hub // +kubebuilder:subresource:status // +versionName=v1 // +kubebuilder:storageversion 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 28fa9d6520b..515f1bb5b25 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 @@ -1,4 +1,6 @@ /* +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 @@ -21,16 +23,17 @@ For imports, we'll need the controller-runtime package, plus the API version for our hub type (v1), and finally some of the standard packages. */ + import ( "fmt" "strings" - "sigs.k8s.io/controller-runtime/pkg/conversion" + "log" - v1 "tutorial.kubebuilder.io/project/api/v1" -) + "sigs.k8s.io/controller-runtime/pkg/conversion" -// +kubebuilder:docs-gen:collapse=Imports + batchv1 "tutorial.kubebuilder.io/project/api/v1" +) // +kubebuilder:docs-gen:collapse=Imports /* Our "spoke" versions need to implement the @@ -43,9 +46,11 @@ methods to convert to/from the hub version. ConvertTo is expected to modify its argument to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ + // ConvertTo converts this CronJob to the Hub version (v1). func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1.CronJob) + dst := dstRaw.(*batchv1.CronJob) + log.Printf("Converting from %T to %T", dst.APIVersion, src.APIVersion) sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} @@ -74,7 +79,7 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = v1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit @@ -85,6 +90,7 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { dst.Status.LastScheduleTime = src.Status.LastScheduleTime // +kubebuilder:docs-gen:collapse=rote conversion + return nil } @@ -93,9 +99,10 @@ ConvertFrom is expected to modify its receiver to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ -// ConvertFrom converts from the Hub version (v1) to this version. +// ConvertFrom converts the Hub version (v1) to this CronJob (v2). func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1.CronJob) + src := srcRaw.(*batchv1.CronJob) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { @@ -133,5 +140,6 @@ func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { dst.Status.LastScheduleTime = src.Status.LastScheduleTime // +kubebuilder:docs-gen:collapse=rote conversion + return nil } diff --git a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go index 9c96522aa58..4364045d0ff 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go @@ -49,6 +49,7 @@ types implement the [Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub) and [Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interfaces, a conversion webhook will be registered. + */ // SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. diff --git a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go index 5ae40bf80a7..1e83472ab20 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go @@ -164,4 +164,14 @@ var _ = Describe("CronJob Webhook", func() { }) }) + Context("When creating CronJob 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() { + // convertedObj := &batchv1.CronJob{} + // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) + // Expect(convertedObj).ToNot(BeNil()) + // }) + }) + }) diff --git a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go index 13664e9e0bf..a8d445c7b6b 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go +++ b/docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go @@ -84,14 +84,4 @@ var _ = Describe("CronJob Webhook", func() { // }) }) - Context("When creating CronJob 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() { - // convertedObj := &batchv2.CronJob{} - // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) - // Expect(convertedObj).ToNot(BeNil()) - // }) - }) - }) diff --git a/docs/book/src/multiversion-tutorial/webhooks.md b/docs/book/src/multiversion-tutorial/webhooks.md index 6b383c31c52..ef3283aeafd 100644 --- a/docs/book/src/multiversion-tutorial/webhooks.md +++ b/docs/book/src/multiversion-tutorial/webhooks.md @@ -3,15 +3,6 @@ Our conversion is in place, so all that's left is to tell controller-runtime about our conversion. -Normally, we'd run - -```shell -kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion -``` - -to scaffold out the webhook setup. However, we've already got webhook -setup, from when we built our defaulting and validating webhooks! - ## Webhook setup... {{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}} diff --git a/docs/book/src/reference/project-config.md b/docs/book/src/reference/project-config.md index 0b9faa2a536..9ad055a54d4 100644 --- a/docs/book/src/reference/project-config.md +++ b/docs/book/src/reference/project-config.md @@ -150,6 +150,7 @@ Now let's check its layout fields definition: | `resources.core` | It is `true` when the group used is from Kubernetes API and the API resource is not defined on the project. | | `resources.external` | It is `true` when the flag `--external-api-path` was used to generated the scaffold for an [External Type][external-type]. | | `resources.webhooks` | Store the webhooks data when the sub-command `create webhook` is used. | +| `resources.webhooks.spoke` | Store the API version that will act as the Spoke with the designated Hub version for conversion webhooks. | | `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | | `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | | `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | diff --git a/hack/docs/internal/multiversion-tutorial/generate_multiversion.go b/hack/docs/internal/multiversion-tutorial/generate_multiversion.go index 9c7c4c5e576..204ddb7bf3b 100644 --- a/hack/docs/internal/multiversion-tutorial/generate_multiversion.go +++ b/hack/docs/internal/multiversion-tutorial/generate_multiversion.go @@ -17,7 +17,6 @@ limitations under the License. package multiversion import ( - "os" "os/exec" "path/filepath" @@ -57,6 +56,27 @@ func (sp *Sample) GenerateSampleProject() { ) hackutils.CheckError("Creating the v2 API without controller", err) + log.Infof("Creating conversion webhook for v1") + err = sp.ctx.CreateWebhook( + "--group", "batch", + "--version", "v1", + "--kind", "CronJob", + "--conversion", + "--spoke", "v2", + "--force", + ) + hackutils.CheckError("Creating conversion webhook for v1", err) + + log.Infof("Workaround to fix the issue with the conversion webhook") + // FIXME: This is a workaround to fix the issue with the conversion webhook + // We should be able to inject the code when we create webhooks with different + // types of webhooks. However, currently, we are not able to do that and we need to + // force. So, we are copying the code from cronjob tutorial to have the code + // implemented. + cmd := exec.Command("cp", "./../../../cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go", "./internal/webhook/v1/cronjob_webhook.go") + _, err = sp.ctx.Run(cmd) + hackutils.CheckError("Copying the code from cronjob tutorial", err) + log.Infof("Creating defaulting and validation webhook for v2") err = sp.ctx.CreateWebhook( "--group", "batch", @@ -64,7 +84,6 @@ func (sp *Sample) GenerateSampleProject() { "--kind", "CronJob", "--defaulting", "--programmatic-validation", - "--conversion", ) hackutils.CheckError("Creating defaulting and validation webhook for v2", err) } @@ -73,16 +92,189 @@ func (sp *Sample) UpdateTutorial() { log.Println("Update tutorial with multiversion code") // Update files according to the multiversion + sp.updateCronjobV1DueForce() sp.updateApiV1() sp.updateApiV2() - sp.updateWebhookV1() sp.updateWebhookV2() - sp.createHubFiles() + sp.updateConversionFiles() sp.updateSampleV2() sp.updateMain() sp.updateDefaultKustomize() } +func (sp *Sample) updateCronjobV1DueForce() { + // FIXME : This is a workaround to fix the issue with the conversion webhook + path := "internal/webhook/v1/cronjob_webhook.go" + err := pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, path), + "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 +[Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub) and +[Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) +interfaces, a conversion webhook will be registered. +`) + hackutils.CheckError("manager fix doc comment", err) + + path = "internal/webhook/v1/cronjob_webhook_test.go" + err = pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, path), + `var ( + obj *batchv1.CronJob + oldObj *batchv1.CronJob + ) + + BeforeEach(func() { + obj = &batchv1.CronJob{} + oldObj = &batchv1.CronJob{} + 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 + })`, + `var ( + obj *batchv1.CronJob + oldObj *batchv1.CronJob + validator CronJobCustomValidator + defaulter CronJobCustomDefaulter + ) + + BeforeEach(func() { + obj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ + Schedule: "*/5 * * * *", + ConcurrencyPolicy: batchv1.AllowConcurrent, + SuccessfulJobsHistoryLimit: new(int32), + FailedJobsHistoryLimit: new(int32), + }, + } + *obj.Spec.SuccessfulJobsHistoryLimit = 3 + *obj.Spec.FailedJobsHistoryLimit = 1 + + oldObj = &batchv1.CronJob{ + Spec: batchv1.CronJobSpec{ + Schedule: "*/5 * * * *", + ConcurrencyPolicy: batchv1.AllowConcurrent, + SuccessfulJobsHistoryLimit: new(int32), + FailedJobsHistoryLimit: new(int32), + }, + } + *oldObj.Spec.SuccessfulJobsHistoryLimit = 3 + *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") + })`) + hackutils.CheckError("fix cronjob v1 tests", err) + + err = pluginutil.InsertCode(filepath.Join(sp.ctx.Dir, path), + `AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + `, + `Context("When creating CronJob under Defaulting Webhook", func() { + It("Should apply defaults when a required field is empty", func() { + By("simulating a scenario where defaults should be applied") + obj.Spec.ConcurrencyPolicy = "" // This should default to AllowConcurrent + obj.Spec.Suspend = nil // This should default to false + obj.Spec.SuccessfulJobsHistoryLimit = nil // This should default to 3 + obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 + + By("calling the Default method to apply defaults") + defaulter.Default(ctx, obj) + + By("checking that the default values are set") + 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") + }) + + It("Should not overwrite fields that are already set", func() { + By("setting fields that would normally get a default") + obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent + obj.Spec.Suspend = new(bool) + *obj.Spec.Suspend = true + obj.Spec.SuccessfulJobsHistoryLimit = new(int32) + *obj.Spec.SuccessfulJobsHistoryLimit = 5 + obj.Spec.FailedJobsHistoryLimit = new(int32) + *obj.Spec.FailedJobsHistoryLimit = 2 + + By("calling the Default method to apply defaults") + defaulter.Default(ctx, obj) + + By("checking that the fields were not overwritten") + 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") + }) + }) + + Context("When creating or updating CronJob under Validating Webhook", func() { + It("Should deny creation if the name is too long", func() { + obj.ObjectMeta.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" + Expect(validator.ValidateCreate(ctx, obj)).Error().To( + MatchError(ContainSubstring("must be no more than 52 characters")), + "Expected name validation to fail for a too-long name") + }) + + It("Should admit creation if the name is valid", func() { + obj.ObjectMeta.Name = "valid-cronjob-name" + Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), + "Expected name validation to pass for a valid name") + }) + + It("Should deny creation if the schedule is invalid", func() { + obj.Spec.Schedule = "invalid-cron-schedule" + Expect(validator.ValidateCreate(ctx, obj)).Error().To( + MatchError(ContainSubstring("Expected exactly 5 fields, found 1: invalid-cron-schedule")), + "Expected spec validation to fail for an invalid schedule") + }) + + It("Should admit creation if the schedule is valid", func() { + obj.Spec.Schedule = "*/5 * * * *" + Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), + "Expected spec validation to pass for a valid schedule") + }) + + It("Should deny update if both name and spec are invalid", func() { + oldObj.ObjectMeta.Name = "valid-cronjob-name" + oldObj.Spec.Schedule = "*/5 * * * *" + + By("simulating an update") + obj.ObjectMeta.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" + obj.Spec.Schedule = "invalid-cron-schedule" + + By("validating an update") + Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().To(HaveOccurred(), + "Expected validation to fail for both name and spec") + }) + + It("Should admit update if both name and spec are valid", func() { + oldObj.ObjectMeta.Name = "valid-cronjob-name" + oldObj.Spec.Schedule = "*/5 * * * *" + + By("simulating an update") + obj.ObjectMeta.Name = "valid-cronjob-name-updated" + obj.Spec.Schedule = "0 0 * * *" + + By("validating an update") + Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil(), + "Expected validation to pass for a valid update") + }) + }) + + `) + hackutils.CheckError("fix cronjob v1 tests after each", err) +} + func (sp *Sample) updateDefaultKustomize() { // Enable CA for Conversion Webhook err := pluginutil.UncommentCode( @@ -91,18 +283,6 @@ func (sp *Sample) updateDefaultKustomize() { hackutils.CheckError("fixing default/kustomization", err) } -func (sp *Sample) updateWebhookV1() { - err := pluginutil.ReplaceInFile( - 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 -[Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub) and -[Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) -interfaces, a conversion webhook will be registered.`, - ) - hackutils.CheckError("replace webhook setup text", err) -} func (sp *Sample) updateSampleV2() { path := filepath.Join(sp.ctx.Dir, "config/samples/batch_v2_cronjob.yaml") oldText := `# TODO(user): Add fields here` @@ -115,29 +295,82 @@ func (sp *Sample) updateSampleV2() { hackutils.CheckError("replacing TODO with sampleV2Code in batch_v2_cronjob.yaml", err) } -func (sp *Sample) createHubFiles() { +func (sp *Sample) updateConversionFiles() { path := filepath.Join(sp.ctx.Dir, "api/v1/cronjob_conversion.go") - _, err := os.Create(path) - hackutils.CheckError("creating conversion file v1", err) - - err = pluginutil.AppendCodeAtTheEnd(path, "") - hackutils.CheckError("creating empty conversion file v1", err) + err := pluginutil.InsertCodeIfNotExist(path, + "limitations under the License.\n*/", + "\n// +kubebuilder:docs-gen:collapse=Apache License") + hackutils.CheckError("appending into hub v1 collapse docs", err) - err = pluginutil.AppendCodeAtTheEnd(path, hubV1Code) - hackutils.CheckError("appending hubV1Code to cronjob_conversion.go", err) + err = pluginutil.ReplaceInFile(path, + "// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!", + hubV1CodeComment) + hackutils.CheckError("adding comment to hub v1", err) path = filepath.Join(sp.ctx.Dir, "api/v2/cronjob_conversion.go") - _, err = os.Create(path) - hackutils.CheckError("creating conversion file v2", err) + err = pluginutil.InsertCodeIfNotExist(path, + "limitations under the License.\n*/", + "\n// +kubebuilder:docs-gen:collapse=Apache License") + hackutils.CheckError("appending into hub v2 collapse docs", err) + + err = pluginutil.InsertCode(path, + "import (", + ` + "fmt" + "strings" + +`) + hackutils.CheckError("adding imports to hub v2", err) - err = pluginutil.AppendCodeAtTheEnd(path, "") - hackutils.CheckError("creating empty conversion file v2", err) + err = pluginutil.InsertCodeIfNotExist(path, + "batchv1 \"tutorial.kubebuilder.io/project/api/v1\"\n)", + `// +kubebuilder:docs-gen:collapse=Imports - err = pluginutil.AppendCodeAtTheEnd(path, hubV2Code) - hackutils.CheckError("appending hubV2Code to cronjob_conversion.go", err) +/* +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. +*/ +`) + hackutils.CheckError("appending into hub v2 collapse docs", err) + + err = pluginutil.ReplaceInFile(path, + "package v2", + hubV2CodeComment) + hackutils.CheckError("adding comment to hub v2", err) + + err = pluginutil.ReplaceInFile(path, + "// TODO: Implement conversion logic from v1 to v2", + hubV2CovertTo) + hackutils.CheckError("replace covertTo at hub v2", err) + + err = pluginutil.ReplaceInFile(path, + "// TODO: Implement conversion logic from v2 to v1", + hubV2ConvertFromCode) + hackutils.CheckError("replace covert from at hub v2", err) + + err = pluginutil.ReplaceInFile(path, + "// ConvertFrom converts the Hub version (v1) to this CronJob (v2).", + `/* +ConvertFrom is expected to modify its receiver to contain the converted object. +Most of the conversion is straightforward copying, except for converting our changed field. +*/ + +// ConvertFrom converts the Hub version (v1) to this CronJob (v2).`) + hackutils.CheckError("replace covert from info at hub v2", err) + + err = pluginutil.ReplaceInFile(path, + "// ConvertTo converts this CronJob to the Hub version (v1).", + `/* +ConvertTo is expected to modify its argument to contain the converted object. +Most of the conversion is straightforward copying, except for converting our changed field. +*/ +// ConvertTo converts this CronJob to the Hub version (v1).`) + hackutils.CheckError("replace covert info at hub v2", err) } func (sp *Sample) updateApiV1() { diff --git a/hack/docs/internal/multiversion-tutorial/hub.go b/hack/docs/internal/multiversion-tutorial/hub.go index e22e4f698ae..696fd5e095d 100644 --- a/hack/docs/internal/multiversion-tutorial/hub.go +++ b/hack/docs/internal/multiversion-tutorial/hub.go @@ -17,50 +17,16 @@ limitations under the License. package multiversion -const hubV1Code = `/* -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. -*/ -// +kubebuilder:docs-gen:collapse=Apache License - -package v1 - +const hubV1CodeComment = ` /* Implementing the hub method is pretty easy -- we just have to add an empty 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. */ - -// Hub marks this type as a conversion hub. -func (*CronJob) Hub() {} ` -const hubV2Code = `/* -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. -*/ -// +kubebuilder:docs-gen:collapse=Apache License - -package v2 +const hubV2CodeComment = `package v2 /* For imports, we'll need the controller-runtime @@ -68,33 +34,9 @@ For imports, we'll need the controller-runtime package, plus the API version for our hub type (v1), and finally some of the standard packages. */ -import ( - "fmt" - "strings" - - "sigs.k8s.io/controller-runtime/pkg/conversion" - - v1 "tutorial.kubebuilder.io/project/api/v1" -) - -// +kubebuilder:docs-gen:collapse=Imports - -/* -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. -*/ - -/* -ConvertTo is expected to modify its argument to contain the converted object. -Most of the conversion is straightforward copying, except for converting our changed field. -*/ -// ConvertTo converts this CronJob to the Hub version (v1). -func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1.CronJob) +` - sched := src.Spec.Schedule +const hubV2CovertTo = `sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} if sched.Minute != nil { scheduleParts[0] = string(*sched.Minute) @@ -121,7 +63,7 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds - dst.Spec.ConcurrencyPolicy = v1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) + dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit @@ -131,20 +73,9 @@ func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime - // +kubebuilder:docs-gen:collapse=rote conversion - return nil -} - -/* -ConvertFrom is expected to modify its receiver to contain the converted object. -Most of the conversion is straightforward copying, except for converting our changed field. -*/ - -// ConvertFrom converts from the Hub version (v1) to this version. -func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1.CronJob) + // +kubebuilder:docs-gen:collapse=rote conversion` - schedParts := strings.Split(src.Spec.Schedule, " ") +const hubV2ConvertFromCode = `schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { return fmt.Errorf("invalid schedule: not a standard 5-field schedule") } @@ -179,6 +110,4 @@ func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime - // +kubebuilder:docs-gen:collapse=rote conversion - return nil -}` + // +kubebuilder:docs-gen:collapse=rote conversion` diff --git a/pkg/cli/alpha/internal/generate.go b/pkg/cli/alpha/internal/generate.go index 2d5cc1c025c..8ee434d109f 100644 --- a/pkg/cli/alpha/internal/generate.go +++ b/pkg/cli/alpha/internal/generate.go @@ -361,15 +361,18 @@ func createWebhook(resource resource.Resource) error { // Gets flags for webhook creation. func getWebhookResourceFlags(resource resource.Resource) []string { var args []string - if resource.HasConversionWebhook() { - args = append(args, "--conversion") - } if resource.HasValidationWebhook() { args = append(args, "--programmatic-validation") } if resource.HasDefaultingWebhook() { args = append(args, "--defaulting") } + if resource.HasConversionWebhook() { + args = append(args, "--conversion") + for _, spoke := range resource.Webhooks.Spoke { + args = append(args, "--spoke", spoke) + } + } return args } diff --git a/pkg/model/resource/resource_test.go b/pkg/model/resource/resource_test.go index 5935d02e2f9..c08e05f8cf7 100644 --- a/pkg/model/resource/resource_test.go +++ b/pkg/model/resource/resource_test.go @@ -234,6 +234,7 @@ var _ = Describe("Resource", func() { Expect(other.Webhooks.Defaulting).To(Equal(res.Webhooks.Defaulting)) Expect(other.Webhooks.Validation).To(Equal(res.Webhooks.Validation)) Expect(other.Webhooks.Conversion).To(Equal(res.Webhooks.Conversion)) + Expect(other.Webhooks.Spoke).To(Equal(res.Webhooks.Spoke)) }) It("modifying the copy should not affect the original", func() { diff --git a/pkg/model/resource/webhooks.go b/pkg/model/resource/webhooks.go index 6d8bc0378aa..fd35c2372e3 100644 --- a/pkg/model/resource/webhooks.go +++ b/pkg/model/resource/webhooks.go @@ -33,6 +33,8 @@ type Webhooks struct { // Conversion specifies if a conversion webhook is associated to the resource. Conversion bool `json:"conversion,omitempty"` + + Spoke []string `json:"spoke,omitempty"` } // Validate checks that the Webhooks is valid. @@ -77,6 +79,9 @@ func (webhooks *Webhooks) Update(other *Webhooks) error { // Update conversion. webhooks.Conversion = webhooks.Conversion || other.Conversion + // Update Spoke. + webhooks.Spoke = append(webhooks.Spoke, other.Spoke...) + return nil } @@ -84,3 +89,23 @@ func (webhooks *Webhooks) Update(other *Webhooks) error { func (webhooks Webhooks) IsEmpty() bool { return webhooks.WebhookVersion == "" && !webhooks.Defaulting && !webhooks.Validation && !webhooks.Conversion } + +// HasSpokeVersion returns true if the spoke version is present in the list of spoke versions. +func (webhooks Webhooks) HasSpokeVersion(version string) bool { + for _, v := range webhooks.Spoke { + if v == version { + return true + } + } + return false +} + +// HasAnySpokeVersionFrom returns true if any spoke versions are present in the list of spoke versions. +func (webhooks Webhooks) HasAnySpokeVersionFrom(values []string) bool { + for _, v := range values { + if webhooks.HasSpokeVersion(v) { + return true + } + } + return false +} diff --git a/pkg/model/resource/webhooks_test.go b/pkg/model/resource/webhooks_test.go index 4b0d6bd9132..1c136ba2419 100644 --- a/pkg/model/resource/webhooks_test.go +++ b/pkg/model/resource/webhooks_test.go @@ -52,12 +52,14 @@ var _ = Describe("Webhooks", func() { Defaulting: true, Validation: true, Conversion: true, + Spoke: []string{"v2"}, } Expect(webhook.Update(nil)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal(v1)) Expect(webhook.Defaulting).To(BeTrue()) Expect(webhook.Validation).To(BeTrue()) Expect(webhook.Conversion).To(BeTrue()) + Expect(webhook.Spoke).To(Equal([]string{"v2"})) }) Context("webhooks version", func() { diff --git a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go index fd871ecffef..9581892b62e 100644 --- a/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go +++ b/pkg/plugins/common/kustomize/v2/scaffolds/webhook.go @@ -19,8 +19,6 @@ package scaffolds import ( "fmt" - "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/patches" - log "github.com/sirupsen/logrus" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" @@ -29,6 +27,7 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd" + "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/patches" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault" network_policy "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/webhook" diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index ea55db3eac4..b7965a713d8 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -72,6 +72,9 @@ type Options struct { DoDefaulting bool DoValidation bool DoConversion bool + + // Spoke version for conversion webhook + Spoke []string } // UpdateResource updates the provided resource with the options @@ -108,6 +111,7 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { } if opts.DoConversion { res.Webhooks.Conversion = true + res.Webhooks.Spoke = opts.Spoke } } diff --git a/pkg/plugins/golang/options_test.go b/pkg/plugins/golang/options_test.go index f77b93560d1..13e4f624255 100644 --- a/pkg/plugins/golang/options_test.go +++ b/pkg/plugins/golang/options_test.go @@ -97,6 +97,7 @@ var _ = Describe("Options", func() { Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) Expect(res.Webhooks.Validation).To(Equal(options.DoValidation)) Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion)) + Expect(res.Webhooks.Spoke).To(Equal(options.Spoke)) Expect(res.Webhooks.IsEmpty()).To(BeFalse()) } else { Expect(res.Webhooks.IsEmpty()).To(BeTrue()) diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go new file mode 100644 index 00000000000..df2e4d4e1e0 --- /dev/null +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go @@ -0,0 +1,72 @@ +/* +Copyright 2022 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 api + +import ( + "path/filepath" + + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/kubebuilder/v4/pkg/machinery" +) + +var _ machinery.Template = &Hub{} + +// Hub scaffolds the file that defines hub +// nolint:maligned +type Hub struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin + + Force bool +} + +// SetTemplateDefaults implements file.Template +func (f *Hub) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_conversion.go") + } else { + f.Path = filepath.Join("api", "%[version]", "%[kind]_conversion.go") + } + } + + f.Path = f.Resource.Replacer().Replace(f.Path) + log.Println(f.Path) + + f.TemplateBody = hubTemplate + + if f.Force { + f.IfExistsAction = machinery.OverwriteFile + } else { + f.IfExistsAction = machinery.SkipFile + } + + return nil +} + +const hubTemplate = `{{ .Boilerplate }} + +package {{ .Resource.Version }} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// Hub marks this type as a conversion hub. +func (*{{ .Resource.Kind }}) Hub() {} +` diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go new file mode 100644 index 00000000000..2ff545892af --- /dev/null +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go @@ -0,0 +1,94 @@ +/* +Copyright 2022 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 api + +import ( + "path/filepath" + + log "github.com/sirupsen/logrus" + "sigs.k8s.io/kubebuilder/v4/pkg/machinery" +) + +var _ machinery.Template = &Spoke{} + +// Spoke scaffolds the file that defines spoke version conversion +type Spoke struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin + + Force bool + SpokeVersion string +} + +// SetTemplateDefaults implements file.Template +func (f *Spoke) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup && f.Resource.Group != "" { + f.Path = filepath.Join("api", "%[group]", f.SpokeVersion, "%[kind]_conversion.go") + } else { + f.Path = filepath.Join("api", f.SpokeVersion, "%[kind]_conversion.go") + } + } + + f.Path = f.Resource.Replacer().Replace(f.Path) + log.Println(f.Path) + + f.TemplateBody = spokeTemplate + + if f.Force { + f.IfExistsAction = machinery.OverwriteFile + } else { + f.IfExistsAction = machinery.SkipFile + } + + return nil +} + +// nolint:lll +const spokeTemplate = `{{ .Boilerplate }} + +package {{ .SpokeVersion }} + +import ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" +) + +// ConvertTo converts this {{ .Resource.Kind }} to the Hub version ({{ .Resource.Version }}). +func (src *{{ .Resource.Kind }}) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + log.Printf("Converting from %T to %T", dst.APIVersion, src.APIVersion) + + // TODO: Implement conversion logic from {{ .Resource.Version }} to {{ .SpokeVersion}} + + return nil +} + +// ConvertFrom converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .SpokeVersion}}). +func (dst *{{ .Resource.Kind }}) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // TODO: Implement conversion logic from {{ .SpokeVersion}} to {{ .Resource.Version }} + + return nil +} +` diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go index eb18ee9459b..7b835251e7e 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go @@ -81,7 +81,7 @@ func (f *Webhook) SetTemplateDefaults() error { if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = machinery.Error + f.IfExistsAction = machinery.SkipFile } f.AdmissionReviewVersions = "v1" diff --git a/pkg/plugins/golang/v4/scaffolds/webhook.go b/pkg/plugins/golang/v4/scaffolds/webhook.go index bcfb74d18b1..7704412378b 100644 --- a/pkg/plugins/golang/v4/scaffolds/webhook.go +++ b/pkg/plugins/golang/v4/scaffolds/webhook.go @@ -18,6 +18,9 @@ package scaffolds import ( "fmt" + "strings" + + "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api" log "github.com/sirupsen/logrus" "github.com/spf13/afero" @@ -102,8 +105,41 @@ func (s *webhookScaffolder) Scaffold() error { } if doConversion { + resourceFilePath := fmt.Sprintf("api/%s/%s_types.go", + s.resource.Version, strings.ToLower(s.resource.Kind)) + if s.config.IsMultiGroup() { + resourceFilePath = fmt.Sprintf("api/%s/%s/%s_types.go", + s.resource.Group, s.resource.Version, + strings.ToLower(s.resource.Kind)) + } + + err = pluginutil.InsertCodeIfNotExist(resourceFilePath, + "// +kubebuilder:object:root=true", + "\n// +kubebuilder:storageversion\n// +kubebuilder:conversion:hub") + if err != nil { + log.Errorf("Unable to insert storage version marker "+ + "(// +kubebuilder:storageversion) and the hub conversion (// +kubebuilder:conversion:hub) "+ + "in file %s: %v", resourceFilePath, err) + } + + if err := scaffold.Execute( + &api.Hub{Force: s.force}, + ); err != nil { + return err + } + + // Create the spoke version conversion file for each spoke version + for _, spokeVersion := range s.resource.Webhooks.Spoke { + if err := scaffold.Execute( + &api.Spoke{Force: s.force, SpokeVersion: spokeVersion}, + ); err != nil { + return err + } + } + log.Println(`Webhook server has been set up for you. You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) + } // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest. diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go index 685b216db9d..5c4d97c5367 100644 --- a/pkg/plugins/golang/v4/webhook.go +++ b/pkg/plugins/golang/v4/webhook.go @@ -65,7 +65,7 @@ validating and/or conversion webhooks. # Create conversion webhook for Group: ship, Version: v1beta1 # and Kind: Frigate - %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1,v2,v3 `, cliMeta.CommandName) } @@ -83,6 +83,10 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.options.DoConversion, "conversion", false, "if set, scaffold the conversion webhook") + pflag.StringSliceVar(&p.options.Spoke, "spoke", + []string{}, + "comma-separated list of spoke versions for conversion webhook (i.e. --spoke v1,v2,v3)") + // TODO: remove for go/v5 fs.BoolVar(&p.isLegacyPath, "legacy", false, "[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+ @@ -109,32 +113,54 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { p.resource = res if len(p.options.ExternalAPIPath) != 0 && len(p.options.ExternalAPIDomain) != 0 && p.isLegacyPath { - return errors.New("You cannot scaffold webhooks for external types " + - "using the legacy path") + return errors.New("you cannot scaffold webhooks for external types using the legacy path") } - p.options.UpdateResource(p.resource, p.config) - - if err := p.resource.Validate(); err != nil { - return err + if p.options.DoConversion { + if len(p.options.Spoke) == 0 { + return fmt.Errorf("conversion webhook requires --spoke version to be specified") + } } - if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() { + if !p.options.DoConversion && !p.options.DoDefaulting && !p.options.DoValidation { return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+ " --programmatic-validation and --conversion to be true", p.commandName) } - // check if resource exist to create webhook + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { + return err + } + resValue, err := p.config.GetResource(p.resource.GVK) res = &resValue if err != nil { + // Resource must be previously created for non-external and non-core resources if !p.resource.External && !p.resource.Core { - return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName) + return fmt.Errorf("%s create webhook requires a previously created API", p.commandName) } } else if res.Webhooks != nil && !res.Webhooks.IsEmpty() && !p.force { - return fmt.Errorf("webhook resource already exists") + // FIXME: This is a temporary fix to allow we move forward + // However, users should be able to call the command to create an webhook + // even if the resource already has one when the webhook is not of the same type. + return fmt.Errorf("webhook of this type already exists") } + if p.options.DoConversion { + if res.HasConversionWebhook() && res.Webhooks.HasAnySpokeVersionFrom(p.options.Spoke) { + return fmt.Errorf("conversion webhook already exists with one or more spoke versions informed") + } + + // Check if all spoke versions informed are api versions which exist in the resource + for _, spokeVersion := range p.options.Spoke { + spokeGKV := res.GVK + spokeGKV.Version = spokeVersion + _, err := p.config.GetResource(spokeGKV) + if err != nil { + return fmt.Errorf("resource does not have a version %s", spokeVersion) + } + } + } return nil } diff --git a/test/e2e/alphagenerate/generate_test.go b/test/e2e/alphagenerate/generate_test.go index 5b896e6f624..99ed8bc26e1 100644 --- a/test/e2e/alphagenerate/generate_test.go +++ b/test/e2e/alphagenerate/generate_test.go @@ -123,6 +123,18 @@ func generateProject(kbc *utils.TestContext) { ) Expect(err).NotTo(HaveOccurred(), "Failed to scaffold API with resource and controller") + By("creating API definition with controller and resource") + err = kbc.CreateAPI( + "--group", "crew", + "--version", "v2", + "--kind", "Memcached", + "--namespaced", + "--resource=true", + "--controller=false", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to scaffold API with resource and controller") + By("creating Webhook for Memcached API") err = kbc.CreateWebhook( "--group", "crew", @@ -131,6 +143,7 @@ func generateProject(kbc *utils.TestContext) { "--defaulting", "--programmatic-validation", "--conversion", + "--spoke", "v2", ) Expect(err).NotTo(HaveOccurred(), "Failed to scaffold webhook for Memcached API") diff --git a/test/e2e/v4/generate_test.go b/test/e2e/v4/generate_test.go index 8b7b9fcdd45..9113d0ae19d 100644 --- a/test/e2e/v4/generate_test.go +++ b/test/e2e/v4/generate_test.go @@ -18,7 +18,6 @@ package v4 import ( "fmt" - "os" "path/filepath" "strings" @@ -396,6 +395,7 @@ func scaffoldConversionWebhook(kbc *utils.TestContext) { "--version", "v1", "--kind", "ConversionTest", "--conversion", + "--spoke v2", "--make=false", ) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion webhook for v1") @@ -416,62 +416,15 @@ func scaffoldConversionWebhook(kbc *utils.TestContext) { "\n\tReplicas int `json:\"replicas,omitempty\"` // Number of replicas", )).NotTo(HaveOccurred(), "failed to add replicas spec to conversiontest_types v2") - // TODO: Remove the code bellow when we have hub and spoke scaffolded by - // Kubebuilder. Intead of create the file we will replace the TODO(user) - // with the code implementation. - By("implementing markers") - ExpectWithOffset(1, pluginutil.InsertCode( - filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"), - "// +kubebuilder:object:root=true\n// +kubebuilder:subresource:status", - "\n// +kubebuilder:storageversion\n// +kubebuilder:conversion:hub\n", - )).NotTo(HaveOccurred(), "failed to add markers to conversiontest_types v1") - - // Create the hub conversion file in v1 - By("creating the conversion implementation in v1 as hub") - err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v1", "conversiontest_conversion.go"), []byte(` -package v1 - -// ConversionTest defines the hub conversion logic. -// Implement the Hub interface to signal that v1 is the hub version. -func (*ConversionTest) Hub() {} -`), 0644) - ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create hub conversion file in v1") - - // Create the conversion file in v2 - By("creating the conversion implementation in v2") - err = os.WriteFile(filepath.Join(kbc.Dir, "api", "v2", "conversiontest_conversion.go"), []byte(` -package v2 - -import ( - "log" - - "sigs.k8s.io/controller-runtime/pkg/conversion" - v1 "sigs.k8s.io/kubebuilder/v4/api/v1" -) - -// ConvertTo converts this ConversionTest to the Hub version (v1). -func (src *ConversionTest) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1.ConversionTest) - log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "internal/webhook/v1/conversiontest_webhook.go"), + "// TODO: Implement conversion logic from v1 to v2", + `dst.Spec.Size = src.Spec.Replicas`) + Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v1 to v2") - // Implement conversion logic from v2 to v1 - dst.Spec.Size = src.Spec.Replicas // Convert replicas in v2 to size in v1 - - return nil -} - -// ConvertFrom converts the Hub version (v1) to this ConversionTest (v2). -func (dst *ConversionTest) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1.ConversionTest) - log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) - - // Implement conversion logic from v1 to v2 - dst.Spec.Replicas = src.Spec.Size // Convert size in v1 to replicas in v2 - - return nil -} -`), 0644) - ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion file in v2") + err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "internal/webhook/v1/conversiontest_webhook.go"), + "// TODO: Implement conversion logic from v2 to v1", + `dst.Spec.Replicas = src.Spec.Size`) + Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v2 to v1") } const monitorTlsPatch = `#patches: diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 5c169bb4017..f33ff8ee174 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -41,16 +41,12 @@ function scaffold_test_project { $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 --make=false - + # Create API to test conversion from v1 to v2 $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false $kb create api --group crew --version v2 --kind FirstMate --controller=false --resource=true --make=false - $kb create webhook --group crew --version v1 --kind FirstMate --conversion --make=false + $kb create webhook --group crew --version v1 --kind FirstMate --conversion --make=false --spoke v2 - # TODO: Remove it when we have the hub and spoke scaffolded by Kubebuilder - # Apply the sed command based on project type - insert_kubebuilder_annotations "api/v1/firstmate_types.go" - $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 # Controller for External types @@ -104,16 +100,7 @@ function scaffold_test_project { # Create API to check webhook --conversion from v1 to v2 $kb create api --group example.com --version v1 --kind Wordpress --controller=true --resource=true --make=false $kb create api --group example.com --version v2 --kind Wordpress --controller=false --resource=true --make=false - $kb create webhook --group example.com --version v1 --kind Wordpress --conversion --make=false - - # TODO: Remove it when we have the hub and spoke scaffolded by Kubebuilder - # Apply the sed command based on project type - if [[ $project =~ multigroup ]]; then - insert_kubebuilder_annotations "api/example.com/v1/wordpress_types.go" - fi - if [[ $project =~ with-plugins ]]; then - insert_kubebuilder_annotations "api/v1/wordpress_types.go" - fi + $kb create webhook --group example.com --version v1 --kind Wordpress --conversion --make=false --spoke v2 header_text 'Editing project with Grafana plugin ...' $kb edit --plugins=grafana.kubebuilder.io/v1-alpha @@ -133,16 +120,6 @@ function scaffold_test_project { popd } -# TODO: Remove when hub and spoke be scaffolded by Kubebuilder -function insert_kubebuilder_annotations { - local file=$1 - local line=43 # The target line to insert text before - local annotations="// +kubebuilder:storageversion\n// +kubebuilder:conversion:hub" - - # Create a temporary file to avoid using -i flag, which varies between macOS and Linux - awk -v insert="$annotations" -v line=$line 'NR==line{print insert} 1' "$file" > "$file.tmp" && mv "$file.tmp" "$file" -} - build_kb scaffold_test_project project-v4 --plugins="go/v4" diff --git a/test/testdata/legacy-webhook-path.sh b/test/testdata/legacy-webhook-path.sh index e702d9d2791..a16f8b8c776 100755 --- a/test/testdata/legacy-webhook-path.sh +++ b/test/testdata/legacy-webhook-path.sh @@ -47,7 +47,8 @@ function scaffold_test_project { $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 v2 --kind FirstMate --controller=false --resource=true --make=false + $kb create webhook --group crew --version v1 --kind FirstMate --conversion --spoke v2 --legacy=true --make=false $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 @@ -61,7 +62,9 @@ function scaffold_test_project { $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 Frigate --controller=false --resource=true --make=false + $kb create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1 --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 diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index c0bc9af9db6..cf50c3a1515 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -183,6 +183,9 @@ resources: version: v1 webhooks: conversion: true + spoke: + - v2 + - v2 webhookVersion: v1 - api: crdVersion: v1 diff --git a/testdata/project-v4-multigroup/api/example.com/v1/wordpress_conversion.go b/testdata/project-v4-multigroup/api/example.com/v1/wordpress_conversion.go new file mode 100644 index 00000000000..1c00f708fe9 --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v1/wordpress_conversion.go @@ -0,0 +1,22 @@ +/* +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 + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// Hub marks this type as a conversion hub. +func (*Wordpress) Hub() {} diff --git a/testdata/project-v4-multigroup/api/example.com/v1/wordpress_types.go b/testdata/project-v4-multigroup/api/example.com/v1/wordpress_types.go index ff5263a7f60..885556fc7f2 100644 --- a/testdata/project-v4-multigroup/api/example.com/v1/wordpress_types.go +++ b/testdata/project-v4-multigroup/api/example.com/v1/wordpress_types.go @@ -39,9 +39,9 @@ type WordpressStatus struct { } // +kubebuilder:object:root=true -// +kubebuilder:subresource:status // +kubebuilder:storageversion // +kubebuilder:conversion:hub +// +kubebuilder:subresource:status // Wordpress is the Schema for the wordpresses API. type Wordpress struct { diff --git a/testdata/project-v4-multigroup/api/example.com/v2/wordpress_conversion.go b/testdata/project-v4-multigroup/api/example.com/v2/wordpress_conversion.go new file mode 100644 index 00000000000..c323a5f1f00 --- /dev/null +++ b/testdata/project-v4-multigroup/api/example.com/v2/wordpress_conversion.go @@ -0,0 +1,45 @@ +/* +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 ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + + examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1" +) + +// ConvertTo converts this Wordpress to the Hub version (v1). +func (src *Wordpress) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*examplecomv1.Wordpress) + log.Printf("Converting from %T to %T", dst.APIVersion, src.APIVersion) + + // TODO: Implement conversion logic from v1 to v2 + + return nil +} + +// ConvertFrom converts the Hub version (v1) to this Wordpress (v2). +func (dst *Wordpress) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*examplecomv1.Wordpress) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // TODO: Implement conversion logic from v2 to v1 + + return nil +} diff --git a/testdata/project-v4-with-plugins/PROJECT b/testdata/project-v4-with-plugins/PROJECT index 0a20eedaa16..cd02c1d7706 100644 --- a/testdata/project-v4-with-plugins/PROJECT +++ b/testdata/project-v4-with-plugins/PROJECT @@ -60,6 +60,9 @@ resources: version: v1 webhooks: conversion: true + spoke: + - v2 + - v2 webhookVersion: v1 - api: crdVersion: v1 diff --git a/testdata/project-v4-with-plugins/api/v1/wordpress_conversion.go b/testdata/project-v4-with-plugins/api/v1/wordpress_conversion.go new file mode 100644 index 00000000000..1c00f708fe9 --- /dev/null +++ b/testdata/project-v4-with-plugins/api/v1/wordpress_conversion.go @@ -0,0 +1,22 @@ +/* +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 + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// Hub marks this type as a conversion hub. +func (*Wordpress) Hub() {} diff --git a/testdata/project-v4-with-plugins/api/v1/wordpress_types.go b/testdata/project-v4-with-plugins/api/v1/wordpress_types.go index ff5263a7f60..885556fc7f2 100644 --- a/testdata/project-v4-with-plugins/api/v1/wordpress_types.go +++ b/testdata/project-v4-with-plugins/api/v1/wordpress_types.go @@ -39,9 +39,9 @@ type WordpressStatus struct { } // +kubebuilder:object:root=true -// +kubebuilder:subresource:status // +kubebuilder:storageversion // +kubebuilder:conversion:hub +// +kubebuilder:subresource:status // Wordpress is the Schema for the wordpresses API. type Wordpress struct { diff --git a/testdata/project-v4-with-plugins/api/v2/wordpress_conversion.go b/testdata/project-v4-with-plugins/api/v2/wordpress_conversion.go new file mode 100644 index 00000000000..298bb4971bb --- /dev/null +++ b/testdata/project-v4-with-plugins/api/v2/wordpress_conversion.go @@ -0,0 +1,45 @@ +/* +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 ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + + examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1" +) + +// ConvertTo converts this Wordpress to the Hub version (v1). +func (src *Wordpress) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*examplecomv1.Wordpress) + log.Printf("Converting from %T to %T", dst.APIVersion, src.APIVersion) + + // TODO: Implement conversion logic from v1 to v2 + + return nil +} + +// ConvertFrom converts the Hub version (v1) to this Wordpress (v2). +func (dst *Wordpress) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*examplecomv1.Wordpress) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // TODO: Implement conversion logic from v2 to v1 + + return nil +} diff --git a/testdata/project-v4/PROJECT b/testdata/project-v4/PROJECT index 96531434b58..193cbadc8c4 100644 --- a/testdata/project-v4/PROJECT +++ b/testdata/project-v4/PROJECT @@ -32,6 +32,9 @@ resources: version: v1 webhooks: conversion: true + spoke: + - v2 + - v2 webhookVersion: v1 - api: crdVersion: v1 diff --git a/testdata/project-v4/api/v1/firstmate_conversion.go b/testdata/project-v4/api/v1/firstmate_conversion.go new file mode 100644 index 00000000000..ae43ee6da78 --- /dev/null +++ b/testdata/project-v4/api/v1/firstmate_conversion.go @@ -0,0 +1,22 @@ +/* +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 + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// Hub marks this type as a conversion hub. +func (*FirstMate) Hub() {} diff --git a/testdata/project-v4/api/v1/firstmate_types.go b/testdata/project-v4/api/v1/firstmate_types.go index 552dfc25988..74c6b1dc0d6 100644 --- a/testdata/project-v4/api/v1/firstmate_types.go +++ b/testdata/project-v4/api/v1/firstmate_types.go @@ -39,9 +39,9 @@ type FirstMateStatus struct { } // +kubebuilder:object:root=true -// +kubebuilder:subresource:status // +kubebuilder:storageversion // +kubebuilder:conversion:hub +// +kubebuilder:subresource:status // FirstMate is the Schema for the firstmates API. type FirstMate struct { diff --git a/testdata/project-v4/api/v2/firstmate_conversion.go b/testdata/project-v4/api/v2/firstmate_conversion.go new file mode 100644 index 00000000000..858fc9dfaea --- /dev/null +++ b/testdata/project-v4/api/v2/firstmate_conversion.go @@ -0,0 +1,45 @@ +/* +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 ( + "log" + + "sigs.k8s.io/controller-runtime/pkg/conversion" + + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" +) + +// ConvertTo converts this FirstMate to the Hub version (v1). +func (src *FirstMate) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*crewv1.FirstMate) + log.Printf("Converting from %T to %T", dst.APIVersion, src.APIVersion) + + // TODO: Implement conversion logic from v1 to v2 + + return nil +} + +// ConvertFrom converts the Hub version (v1) to this FirstMate (v2). +func (dst *FirstMate) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*crewv1.FirstMate) + log.Printf("Converting from %T to %T", src.APIVersion, dst.APIVersion) + + // TODO: Implement conversion logic from v2 to v1 + + return nil +}