Skip to content

Commit 84b24b5

Browse files
authored
feat: Added support for setting env vars in feast services in feast controller (feast-dev#4739)
Add support for setting env vars in services Signed-off-by: Theodor Mihalache <[email protected]>
1 parent b72c2da commit 84b24b5

File tree

7 files changed

+2233
-25
lines changed

7 files changed

+2233
-25
lines changed

infra/feast-operator/api/v1alpha1/featurestore_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ type DefaultConfigs struct {
121121

122122
// OptionalConfigs k8s container settings that are optional
123123
type OptionalConfigs struct {
124+
Env *[]corev1.EnvVar `json:"env,omitempty"`
124125
ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
125126
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`
126127
}

infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml

Lines changed: 685 additions & 0 deletions
Large diffs are not rendered by default.

infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml

Lines changed: 685 additions & 0 deletions
Large diffs are not rendered by default.

infra/feast-operator/dist/install.yaml

Lines changed: 685 additions & 0 deletions
Large diffs are not rendered by default.

infra/feast-operator/internal/controller/featurestore_controller_test.go

Lines changed: 141 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"encoding/base64"
22+
"reflect"
2223

2324
. "github.com/onsi/ginkgo/v2"
2425
. "github.com/onsi/gomega"
@@ -382,7 +383,9 @@ var _ = Describe("FeatureStore Controller", func() {
382383
Context("When reconciling a resource with all services enabled", func() {
383384
const resourceName = "services"
384385
image := "test:latest"
385-
var pullPolicy corev1.PullPolicy = corev1.PullAlways
386+
var pullPolicy = corev1.PullAlways
387+
var testEnvVarName = "testEnvVarName"
388+
var testEnvVarValue = "testEnvVarValue"
386389

387390
ctx := context.Background()
388391

@@ -396,29 +399,8 @@ var _ = Describe("FeatureStore Controller", func() {
396399
By("creating the custom resource for the Kind FeatureStore")
397400
err := k8sClient.Get(ctx, typeNamespacedName, featurestore)
398401
if err != nil && errors.IsNotFound(err) {
399-
resource := &feastdevv1alpha1.FeatureStore{
400-
ObjectMeta: metav1.ObjectMeta{
401-
Name: resourceName,
402-
Namespace: "default",
403-
},
404-
Spec: feastdevv1alpha1.FeatureStoreSpec{
405-
FeastProject: feastProject,
406-
Services: &feastdevv1alpha1.FeatureStoreServices{
407-
OfflineStore: &feastdevv1alpha1.OfflineStore{},
408-
OnlineStore: &feastdevv1alpha1.OnlineStore{
409-
ServiceConfigs: feastdevv1alpha1.ServiceConfigs{
410-
DefaultConfigs: feastdevv1alpha1.DefaultConfigs{
411-
Image: &image,
412-
},
413-
OptionalConfigs: feastdevv1alpha1.OptionalConfigs{
414-
ImagePullPolicy: &pullPolicy,
415-
Resources: &corev1.ResourceRequirements{},
416-
},
417-
},
418-
},
419-
},
420-
},
421-
}
402+
resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue},
403+
{Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}})
422404
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
423405
}
424406
})
@@ -463,6 +445,7 @@ var _ = Describe("FeatureStore Controller", func() {
463445
Expect(resource.Status.Applied.Services.OfflineStore.Resources).To(BeNil())
464446
Expect(resource.Status.Applied.Services.OfflineStore.Image).To(Equal(&services.DefaultImage))
465447
Expect(resource.Status.Applied.Services.OnlineStore).NotTo(BeNil())
448+
Expect(resource.Status.Applied.Services.OnlineStore.Env).To(Equal(&[]corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}))
466449
Expect(resource.Status.Applied.Services.OnlineStore.ImagePullPolicy).To(Equal(&pullPolicy))
467450
Expect(resource.Status.Applied.Services.OnlineStore.Resources).NotTo(BeNil())
468451
Expect(resource.Status.Applied.Services.OnlineStore.Image).To(Equal(&image))
@@ -658,7 +641,7 @@ var _ = Describe("FeatureStore Controller", func() {
658641
deploy)
659642
Expect(err).NotTo(HaveOccurred())
660643
Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1))
661-
Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1))
644+
Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3))
662645
Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways))
663646
env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env)
664647
Expect(env).NotTo(BeNil())
@@ -689,6 +672,7 @@ var _ = Describe("FeatureStore Controller", func() {
689672
Registry: regRemote,
690673
}
691674
Expect(repoConfigOnline).To(Equal(onlineConfig))
675+
Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3))
692676

693677
// check client config
694678
cm := &corev1.ConfigMap{}
@@ -751,6 +735,89 @@ var _ = Describe("FeatureStore Controller", func() {
751735
Expect(repoConfig).To(Equal(testConfig))
752736
})
753737

738+
It("should properly set container env variables", func() {
739+
By("Reconciling the created resource")
740+
controllerReconciler := &FeatureStoreReconciler{
741+
Client: k8sClient,
742+
Scheme: k8sClient.Scheme(),
743+
}
744+
745+
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
746+
NamespacedName: typeNamespacedName,
747+
})
748+
Expect(err).NotTo(HaveOccurred())
749+
750+
resource := &feastdevv1alpha1.FeatureStore{}
751+
err = k8sClient.Get(ctx, typeNamespacedName, resource)
752+
Expect(err).NotTo(HaveOccurred())
753+
754+
req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name})
755+
Expect(err).NotTo(HaveOccurred())
756+
labelSelector := labels.NewSelector().Add(*req)
757+
listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector}
758+
deployList := appsv1.DeploymentList{}
759+
err = k8sClient.List(ctx, &deployList, listOpts)
760+
Expect(err).NotTo(HaveOccurred())
761+
Expect(deployList.Items).To(HaveLen(3))
762+
763+
svcList := corev1.ServiceList{}
764+
err = k8sClient.List(ctx, &svcList, listOpts)
765+
Expect(err).NotTo(HaveOccurred())
766+
Expect(svcList.Items).To(HaveLen(3))
767+
768+
cmList := corev1.ConfigMapList{}
769+
err = k8sClient.List(ctx, &cmList, listOpts)
770+
Expect(err).NotTo(HaveOccurred())
771+
Expect(cmList.Items).To(HaveLen(1))
772+
773+
feast := services.FeastServices{
774+
Client: controllerReconciler.Client,
775+
Context: ctx,
776+
Scheme: controllerReconciler.Scheme,
777+
FeatureStore: resource,
778+
}
779+
780+
fsYamlStr := ""
781+
fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType)
782+
Expect(err).NotTo(HaveOccurred())
783+
784+
// check online config
785+
deploy := &appsv1.Deployment{}
786+
err = k8sClient.Get(ctx, types.NamespacedName{
787+
Name: feast.GetFeastServiceName(services.OnlineFeastType),
788+
Namespace: resource.Namespace,
789+
},
790+
deploy)
791+
Expect(err).NotTo(HaveOccurred())
792+
Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1))
793+
Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3))
794+
Expect(areEnvVarArraysEqual(deploy.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue}, {Name: services.FeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}})).To(BeTrue())
795+
Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways))
796+
797+
// change feast project and reconcile
798+
resourceNew := resource.DeepCopy()
799+
resourceNew.Spec.Services.OnlineStore.Env = &[]corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue + "1"}, {Name: services.FeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}}
800+
err = k8sClient.Update(ctx, resourceNew)
801+
Expect(err).NotTo(HaveOccurred())
802+
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
803+
NamespacedName: typeNamespacedName,
804+
})
805+
Expect(err).NotTo(HaveOccurred())
806+
807+
err = k8sClient.Get(ctx, typeNamespacedName, resource)
808+
Expect(err).NotTo(HaveOccurred())
809+
Expect(areEnvVarArraysEqual(*resource.Status.Applied.Services.OnlineStore.Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue + "1"}, {Name: services.FeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}})).To(BeTrue())
810+
err = k8sClient.Get(ctx, types.NamespacedName{
811+
Name: feast.GetFeastServiceName(services.OnlineFeastType),
812+
Namespace: resource.Namespace,
813+
},
814+
deploy)
815+
Expect(err).NotTo(HaveOccurred())
816+
817+
Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3))
818+
Expect(areEnvVarArraysEqual(deploy.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue + "1"}, {Name: services.FeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}}}})).To(BeTrue())
819+
})
820+
754821
It("Should delete k8s objects owned by the FeatureStore CR", func() {
755822
By("changing which feast services are configured in the CR")
756823
controllerReconciler := &FeatureStoreReconciler{
@@ -1122,6 +1189,33 @@ var _ = Describe("FeatureStore Controller", func() {
11221189
})
11231190
})
11241191

1192+
func createFeatureStoreResource(resourceName string, image string, pullPolicy corev1.PullPolicy, envVars *[]corev1.EnvVar) *feastdevv1alpha1.FeatureStore {
1193+
return &feastdevv1alpha1.FeatureStore{
1194+
ObjectMeta: metav1.ObjectMeta{
1195+
Name: resourceName,
1196+
Namespace: "default",
1197+
},
1198+
Spec: feastdevv1alpha1.FeatureStoreSpec{
1199+
FeastProject: feastProject,
1200+
Services: &feastdevv1alpha1.FeatureStoreServices{
1201+
OfflineStore: &feastdevv1alpha1.OfflineStore{},
1202+
OnlineStore: &feastdevv1alpha1.OnlineStore{
1203+
ServiceConfigs: feastdevv1alpha1.ServiceConfigs{
1204+
DefaultConfigs: feastdevv1alpha1.DefaultConfigs{
1205+
Image: &image,
1206+
},
1207+
OptionalConfigs: feastdevv1alpha1.OptionalConfigs{
1208+
Env: envVars,
1209+
ImagePullPolicy: &pullPolicy,
1210+
Resources: &corev1.ResourceRequirements{},
1211+
},
1212+
},
1213+
},
1214+
},
1215+
},
1216+
}
1217+
}
1218+
11251219
func getFeatureStoreYamlEnvVar(envs []corev1.EnvVar) *corev1.EnvVar {
11261220
for _, e := range envs {
11271221
if e.Name == services.FeatureStoreYamlEnvVar {
@@ -1130,3 +1224,25 @@ func getFeatureStoreYamlEnvVar(envs []corev1.EnvVar) *corev1.EnvVar {
11301224
}
11311225
return nil
11321226
}
1227+
1228+
func areEnvVarArraysEqual(arr1 []corev1.EnvVar, arr2 []corev1.EnvVar) bool {
1229+
if len(arr1) != len(arr2) {
1230+
return false
1231+
}
1232+
1233+
// Create a map to count occurrences of EnvVars in the first array.
1234+
envMap := make(map[string]corev1.EnvVar)
1235+
1236+
for _, env := range arr1 {
1237+
envMap[env.Name] = env
1238+
}
1239+
1240+
// Check the second array against the map.
1241+
for _, env := range arr2 {
1242+
if _, exists := envMap[env.Name]; !exists || !reflect.DeepEqual(envMap[env.Name], env) {
1243+
return false
1244+
}
1245+
}
1246+
1247+
return true
1248+
}

infra/feast-operator/internal/controller/services/services.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,35 @@ func (feast *FeastServices) deleteOwnedFeastObj(obj client.Object) error {
389389
}
390390

391391
func applyOptionalContainerConfigs(container *corev1.Container, optionalConfigs feastdevv1alpha1.OptionalConfigs) {
392+
if optionalConfigs.Env != nil {
393+
container.Env = mergeEnvVarsArrays(container.Env, optionalConfigs.Env)
394+
}
392395
if optionalConfigs.ImagePullPolicy != nil {
393396
container.ImagePullPolicy = *optionalConfigs.ImagePullPolicy
394397
}
395398
if optionalConfigs.Resources != nil {
396399
container.Resources = *optionalConfigs.Resources
397400
}
398401
}
402+
403+
func mergeEnvVarsArrays(envVars1 []corev1.EnvVar, envVars2 *[]corev1.EnvVar) []corev1.EnvVar {
404+
merged := make(map[string]corev1.EnvVar)
405+
406+
// Add all env vars from the first array
407+
for _, envVar := range envVars1 {
408+
merged[envVar.Name] = envVar
409+
}
410+
411+
// Add all env vars from the second array, overriding duplicates
412+
for _, envVar := range *envVars2 {
413+
merged[envVar.Name] = envVar
414+
}
415+
416+
// Convert the map back to an array
417+
result := make([]corev1.EnvVar, 0, len(merged))
418+
for _, envVar := range merged {
419+
result = append(result, envVar)
420+
}
421+
422+
return result
423+
}

0 commit comments

Comments
 (0)