From 4d18e3823b3d393b149fd2be5177eaad160b8f82 Mon Sep 17 00:00:00 2001 From: Frederic Kayser Date: Tue, 28 Oct 2025 17:32:31 +0100 Subject: [PATCH 1/5] feat: Add annotations to cronjob CRDs Signed-off-by: Frederic Kayser --- .../config/crd/bases/feast.dev_featurestores.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index c964d46c27d..9264cfecf49 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -87,6 +87,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. @@ -4063,6 +4068,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. From 6032da78229175264aaf090a5611ca32d4c0b64a Mon Sep 17 00:00:00 2001 From: Frederic Kayser Date: Tue, 28 Oct 2025 20:39:53 +0100 Subject: [PATCH 2/5] Implement handling of annotations Signed-off-by: Frederic Kayser --- .../api/v1alpha1/featurestore_types.go | 3 + .../internal/controller/services/cronjob.go | 8 ++ .../test/api/featurestore_types_test.go | 101 ++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 9250309b1cc..243827a487c 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -111,6 +111,9 @@ type FeastInitOptions struct { // FeastCronJob defines a CronJob to execute against a Feature Store deployment. type FeastCronJob struct { + // Annotations to be added to the CronJob metadata. + Annotations map[string]string `json:"annotations,omitempty"` + // Specification of the desired behavior of a job. JobSpec *JobSpec `json:"jobSpec,omitempty"` ContainerConfigs *CronJobContainerConfigs `json:"containerConfigs,omitempty"` diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index f15200d22ec..3b2c24064d5 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -54,6 +54,14 @@ func (feast *FeastServices) initCronJob() *batchv1.CronJob { func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { appliedCronJob := feast.Handler.FeatureStore.Status.Applied.CronJob cronJob.Labels = feast.getFeastTypeLabels(CronJobFeastType) + if appliedCronJob.Annotations != nil { + if cronJob.Annotations == nil { + cronJob.Annotations = make(map[string]string) + } + for k, v := range appliedCronJob.Annotations { + cronJob.Annotations[k] = v + } + } cronJob.Spec = batchv1.CronJobSpec{ Schedule: appliedCronJob.Schedule, JobTemplate: batchv1.JobTemplateSpec{ diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 83ac2906ec0..e8b08b549d0 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -438,6 +438,35 @@ func registryWithGRPCFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastde return fsCopy } +func cronJobWithAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + Annotations: map[string]string{ + "test-annotation": "test-value", + "another-annotation": "another-value", + }, + Schedule: "0 0 * * *", + } + return fsCopy +} + +func cronJobWithEmptyAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + Annotations: map[string]string{}, + Schedule: "0 0 * * *", + } + return fsCopy +} + +func cronJobWithoutAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + Schedule: "0 0 * * *", + } + return fsCopy +} + func quotedSlice(stringSlice []string) string { quotedSlice := make([]string, len(stringSlice)) @@ -645,4 +674,76 @@ var _ = Describe("FeatureStore API", func() { }) }) }) + + Context("When creating a CronJob", func() { + ctx := context.Background() + + BeforeEach(func() { + By("verifying the custom resource FeatureStore is not there") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err != nil && errors.IsNotFound(err)).To(BeTrue()) + }) + AfterEach(func() { + By("Cleaning up the test resource") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + if err == nil { + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + } + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err != nil && errors.IsNotFound(err)).To(BeTrue()) + }) + + Context("with annotations", func() { + It("should succeed when annotations are provided", func() { + featurestore := createFeatureStore() + resource := cronJobWithAnnotations(featurestore) + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + It("should succeed when annotations are empty", func() { + featurestore := createFeatureStore() + resource := cronJobWithEmptyAnnotations(featurestore) + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + It("should succeed when annotations are not specified", func() { + featurestore := createFeatureStore() + resource := cronJobWithoutAnnotations(featurestore) + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + It("should apply the annotations correctly in the status", func() { + featurestore := createFeatureStore() + resource := cronJobWithAnnotations(featurestore) + services.ApplyDefaultsToStatus(resource) + + Expect(resource.Status.Applied.CronJob).NotTo(BeNil()) + Expect(resource.Status.Applied.CronJob.Annotations).NotTo(BeNil()) + Expect(resource.Status.Applied.CronJob.Annotations).To(HaveLen(2)) + Expect(resource.Status.Applied.CronJob.Annotations["test-annotation"]).To(Equal("test-value")) + Expect(resource.Status.Applied.CronJob.Annotations["another-annotation"]).To(Equal("another-value")) + }) + + It("should keep empty annotations in the status", func() { + featurestore := createFeatureStore() + resource := cronJobWithEmptyAnnotations(featurestore) + services.ApplyDefaultsToStatus(resource) + + Expect(resource.Status.Applied.CronJob).NotTo(BeNil()) + Expect(resource.Status.Applied.CronJob.Annotations).NotTo(BeNil()) + Expect(resource.Status.Applied.CronJob.Annotations).To(BeEmpty()) + }) + + It("should have nil annotations in status when not specified", func() { + featurestore := createFeatureStore() + resource := cronJobWithoutAnnotations(featurestore) + services.ApplyDefaultsToStatus(resource) + + Expect(resource.Status.Applied.CronJob).NotTo(BeNil()) + Expect(resource.Status.Applied.CronJob.Annotations).To(BeNil()) + }) + }) + }) }) From 7ca033a40a4a983727d36d13a012e6b13bb043af Mon Sep 17 00:00:00 2001 From: Frederic Kayser Date: Tue, 28 Oct 2025 20:48:47 +0100 Subject: [PATCH 3/5] Add auto-generated files Signed-off-by: Frederic Kayser --- .../api/v1alpha1/zz_generated.deepcopy.go | 7 +++++++ infra/feast-operator/dist/install.yaml | 10 ++++++++++ infra/feast-operator/docs/api/markdown/ref.md | 1 + 3 files changed, 18 insertions(+) diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 7ea04929b3d..15c61cc86d6 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -114,6 +114,13 @@ func (in *DefaultCtrConfigs) DeepCopy() *DefaultCtrConfigs { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeastCronJob) DeepCopyInto(out *FeastCronJob) { *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.JobSpec != nil { in, out := &in.JobSpec, &out.JobSpec *out = new(JobSpec) diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 58886675ec1..102abd70e3b 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -95,6 +95,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. @@ -4071,6 +4076,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index fac7ebfa784..6016a70a1b8 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -98,6 +98,7 @@ _Appears in:_ | Field | Description | | --- | --- | +| `annotations` _object (keys:string, values:string)_ | Annotations to be added to the CronJob metadata. | | `jobSpec` _[JobSpec](#jobspec)_ | Specification of the desired behavior of a job. | | `containerConfigs` _[CronJobContainerConfigs](#cronjobcontainerconfigs)_ | | | `schedule` _string_ | The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. | From f960b54f4af2e048cd2c15f64c9b500193b5052b Mon Sep 17 00:00:00 2001 From: Frederic Kayser Date: Tue, 28 Oct 2025 20:59:05 +0100 Subject: [PATCH 4/5] Implement changes from review Signed-off-by: Frederic Kayser --- infra/feast-operator/internal/controller/services/cronjob.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index 3b2c24064d5..ff5a98f24df 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -59,7 +59,9 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { cronJob.Annotations = make(map[string]string) } for k, v := range appliedCronJob.Annotations { - cronJob.Annotations[k] = v + if _, exists := cronJob.Annotations[k]; !exists { + cronJob.Annotations[k] = v + } } } cronJob.Spec = batchv1.CronJobSpec{ From 432aab3981271e97162dc139c9adf39d567747b1 Mon Sep 17 00:00:00 2001 From: Frederic Kayser Date: Tue, 28 Oct 2025 21:08:47 +0100 Subject: [PATCH 5/5] Implement changes from review Signed-off-by: Frederic Kayser --- .../internal/controller/services/cronjob.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index ff5a98f24df..b368133b113 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -55,13 +55,9 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { appliedCronJob := feast.Handler.FeatureStore.Status.Applied.CronJob cronJob.Labels = feast.getFeastTypeLabels(CronJobFeastType) if appliedCronJob.Annotations != nil { - if cronJob.Annotations == nil { - cronJob.Annotations = make(map[string]string) - } + cronJob.Annotations = make(map[string]string, len(appliedCronJob.Annotations)) for k, v := range appliedCronJob.Annotations { - if _, exists := cronJob.Annotations[k]; !exists { - cronJob.Annotations[k] = v - } + cronJob.Annotations[k] = v } } cronJob.Spec = batchv1.CronJobSpec{