Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement handling of annotations
Signed-off-by: Frederic Kayser <frederic@frederic-kayser.de>
  • Loading branch information
FredericKayser committed Oct 29, 2025
commit 6032da78229175264aaf090a5611ca32d4c0b64a
3 changes: 3 additions & 0 deletions infra/feast-operator/api/v1alpha1/featurestore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
FredericKayser marked this conversation as resolved.
}
}
cronJob.Spec = batchv1.CronJobSpec{
Schedule: appliedCronJob.Schedule,
JobTemplate: batchv1.JobTemplateSpec{
Expand Down
101 changes: 101 additions & 0 deletions infra/feast-operator/test/api/featurestore_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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())
})
})
})
})