diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 0283c53c96..2b8e818d22 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -78,6 +78,8 @@ type FeatureStoreServices struct { DeploymentStrategy *appsv1.DeploymentStrategy `json:"deploymentStrategy,omitempty"` // Disable the 'feast repo initialization' initContainer DisableInitContainers bool `json:"disableInitContainers,omitempty"` + // Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). + Volumes []corev1.Volume `json:"volumes,omitempty"` } // OfflineStore configures the deployed offline store service @@ -89,6 +91,11 @@ type OfflineStore struct { // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical LogLevel string `json:"logLevel,omitempty"` + // VolumeMounts defines the list of volumes that should be mounted into the feast container. + // This allows attaching persistent storage, config files, secrets, or other resources + // required by the Feast components. Ensure that each volume mount has a corresponding + // volume definition in the Volumes field. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } // OfflineStorePersistence configures the persistence settings for the offline store service @@ -142,6 +149,12 @@ type OnlineStore struct { // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical LogLevel string `json:"logLevel,omitempty"` + + // VolumeMounts defines the list of volumes that should be mounted into the feast container. + // This allows attaching persistent storage, config files, secrets, or other resources + // required by the Feast components. Ensure that each volume mount has a corresponding + // volume definition in the Volumes field. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } // OnlineStorePersistence configures the persistence settings for the online store service @@ -151,7 +164,7 @@ type OnlineStorePersistence struct { DBPersistence *OnlineStoreDBStorePersistence `json:"store,omitempty"` } -// OnlineStoreFilePersistence configures the file-based persistence for the offline store service +// OnlineStoreFilePersistence configures the file-based persistence for the online store service // +kubebuilder:validation:XValidation:rule="(!has(self.pvc) && has(self.path)) ? self.path.startsWith('/') : true",message="Ephemeral stores must have absolute paths." // +kubebuilder:validation:XValidation:rule="(has(self.pvc) && has(self.path)) ? !self.path.startsWith('/') : true",message="PVC path must be a file name only, with no slashes." // +kubebuilder:validation:XValidation:rule="has(self.path) ? !(self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="Online store does not support S3 or GS buckets." @@ -160,7 +173,7 @@ type OnlineStoreFilePersistence struct { PvcConfig *PvcConfig `json:"pvc,omitempty"` } -// OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service +// OnlineStoreDBStorePersistence configures the DB store persistence for the online store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase;milvus Type string `json:"type"` @@ -198,6 +211,11 @@ type LocalRegistryConfig struct { // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical LogLevel string `json:"logLevel,omitempty"` + // VolumeMounts defines the list of volumes that should be mounted into the feast container. + // This allows attaching persistent storage, config files, secrets, or other resources + // required by the Feast components. Ensure that each volume mount has a corresponding + // volume definition in the Volumes field. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } // RegistryPersistence configures the persistence settings for the registry service @@ -290,6 +308,11 @@ type UIService struct { // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical LogLevel string `json:"logLevel,omitempty"` + // VolumeMounts defines the list of volumes that should be mounted into the feast UI container. + // This allows attaching persistent storage, config files, secrets, or other resources + // required by the Feast components. Ensure that each volume mount has a corresponding + // volume definition in the Volumes field. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } // FeatureStoreRef defines which existing FeatureStore's registry should be used diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 815b6397fb..71875d92d9 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -174,6 +174,13 @@ func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { *out = new(v1.DeploymentStrategy) (*in).DeepCopyInto(*out) } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreServices. @@ -269,6 +276,13 @@ func (in *LocalRegistryConfig) DeepCopyInto(out *LocalRegistryConfig) { *out = new(TlsConfigs) (*in).DeepCopyInto(*out) } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRegistryConfig. @@ -295,6 +309,13 @@ func (in *OfflineStore) DeepCopyInto(out *OfflineStore) { *out = new(TlsConfigs) (*in).DeepCopyInto(*out) } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStore. @@ -398,6 +419,13 @@ func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = new(TlsConfigs) (*in).DeepCopyInto(*out) } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStore. @@ -794,6 +822,13 @@ func (in *UIService) DeepCopyInto(out *UIService) { *out = new(TlsConfigs) (*in).DeepCopyInto(*out) } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIService. diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 9d903d050a..8f1ecb1cd2 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -95,6 +95,7 @@ _Appears in:_ | `ui` _[UIService](#uiservice)_ | | | `deploymentStrategy` _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentstrategy-v1-apps)_ | | | `disableInitContainers` _boolean_ | Disable the 'feast repo initialization' initContainer | +| `volumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | Volumes specifies the list of volumes to mount in the FeatureStore deployment. | #### FeatureStoreSpec @@ -195,6 +196,7 @@ _Appears in:_ | `tls` _[TlsConfigs](#tlsconfigs)_ | | | `logLevel` _string_ | LogLevel sets the logging level for the offline store service Allowed values: "debug", "info", "warning", "error", "critical". | +| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | | #### OfflineStoreDBStorePersistence @@ -278,13 +280,14 @@ _Appears in:_ | `tls` _[TlsConfigs](#tlsconfigs)_ | | | `logLevel` _string_ | LogLevel sets the logging level for the online store service Allowed values: "debug", "info", "warning", "error", "critical". | +| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | | #### OnlineStoreDBStorePersistence -OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service +OnlineStoreDBStorePersistence configures the DB store persistence for the online store service _Appears in:_ - [OnlineStorePersistence](#onlinestorepersistence) @@ -300,7 +303,7 @@ _Appears in:_ -OnlineStoreFilePersistence configures the file-based persistence for the offline store service +OnlineStoreFilePersistence configures the file-based persistence for the online store service _Appears in:_ - [OnlineStorePersistence](#onlinestorepersistence) @@ -375,6 +378,7 @@ _Appears in:_ | --- | --- | | `local` _[LocalRegistryConfig](#localregistryconfig)_ | | | `remote` _[RemoteRegistryConfig](#remoteregistryconfig)_ | | +| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | | #### RegistryDBStorePersistence @@ -537,5 +541,6 @@ _Appears in:_ | `tls` _[TlsConfigs](#tlsconfigs)_ | | | `logLevel` _string_ | LogLevel sets the logging level for the UI service Allowed values: "debug", "info", "warning", "error", "critical". | +| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | | diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go index 0e003304a5..2ca5205e2d 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go @@ -104,7 +104,25 @@ func createFeatureStoreResource(resourceName string, image string, pullPolicy co Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: feastProject, Services: &feastdevv1alpha1.FeatureStoreServices{ + Volumes: []corev1.Volume{ + { + Name: "config-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-config", + }, + }, + }, + }, + }, OfflineStore: &feastdevv1alpha1.OfflineStore{ + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config-volume", + MountPath: "/etc/service-config", + }, + }, ServiceConfigs: feastdevv1alpha1.ServiceConfigs{ OptionalConfigs: feastdevv1alpha1.OptionalConfigs{ EnvFrom: envFromVar, @@ -112,6 +130,12 @@ func createFeatureStoreResource(resourceName string, image string, pullPolicy co }, }, OnlineStore: &feastdevv1alpha1.OnlineStore{ + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config-volume", + MountPath: "/etc/service-config", + }, + }, ServiceConfigs: feastdevv1alpha1.ServiceConfigs{ DefaultConfigs: feastdevv1alpha1.DefaultConfigs{ Image: &image, diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 5a2ed7aa4e..8b5396d74c 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -325,6 +325,13 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, fea } func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { + var volumes []corev1.Volume + if feast.Handler.FeatureStore.Spec.Services != nil { + volumes = feast.Handler.FeatureStore.Spec.Services.Volumes + } + if volumes == nil { + volumes = []corev1.Volume{} // Ensure it's an empty slice instead of nil + } deploy.Labels = feast.getLabels() deploy.Spec = appsv1.DeploymentSpec{ Replicas: &DefaultReplicas, @@ -336,6 +343,7 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { }, Spec: corev1.PodSpec{ ServiceAccountName: feast.initFeastSA().Name, + Volumes: volumes, }, }, } @@ -379,7 +387,8 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { return nil } -func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastType FeastServiceType, fsYamlB64 string) { +func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastType FeastServiceType, + fsYamlB64 string) { tls := feast.getTlsConfigs(feastType) serviceConfigs := feast.getServiceConfigs(feastType) defaultServiceConfigs := serviceConfigs.DefaultConfigs @@ -416,11 +425,40 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy ProbeHandler: probeHandler, PeriodSeconds: 10, }, + VolumeMounts: feast.getVolumeMounts(feastType), } applyOptionalContainerConfigs(container, serviceConfigs.OptionalConfigs) *containers = append(*containers, *container) } +func (feast *FeastServices) getVolumeMounts(feastType FeastServiceType) (volumeMounts []corev1.VolumeMount) { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + if appliedServices == nil { + return []corev1.VolumeMount{} // Return an empty slice to avoid nil issues + } + + switch feastType { + case OfflineFeastType: + if feast.isOfflinStore() { + return appliedServices.OfflineStore.VolumeMounts + } + case OnlineFeastType: + if feast.isOnlinStore() { + return appliedServices.OnlineStore.VolumeMounts + } + case RegistryFeastType: + if feast.isLocalRegistry() { + return appliedServices.Registry.Local.VolumeMounts + } + case UIFeastType: + if feast.isUI() { + return appliedServices.UI.VolumeMounts + } + } + + return []corev1.VolumeMount{} // Default empty slice +} + func (feast *FeastServices) setRoute(route *routev1.Route, feastType FeastServiceType) error { svcName := feast.GetFeastServiceName(feastType)