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/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/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. 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. | diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index f15200d22ec..b368133b113 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -54,6 +54,12 @@ 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 { + cronJob.Annotations = make(map[string]string, len(appliedCronJob.Annotations)) + 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()) + }) + }) + }) })