Skip to content

Commit 9ed3c41

Browse files
Squashed volume and volumeMounts commits for rebase
Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com>
1 parent b16fb40 commit 9ed3c41

4 files changed

Lines changed: 353 additions & 2 deletions

File tree

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ type FeatureStoreServices struct {
8181
DeploymentStrategy *appsv1.DeploymentStrategy `json:"deploymentStrategy,omitempty"`
8282
// Disable the 'feast repo initialization' initContainer
8383
DisableInitContainers bool `json:"disableInitContainers,omitempty"`
84+
// 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).
85+
Volumes []corev1.Volume `json:"volumes,omitempty"`
8486
}
8587

8688
// OfflineStore configures the deployed offline store service
@@ -112,6 +114,7 @@ var ValidOfflineStoreFilePersistenceTypes = []string{
112114

113115
// OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service
114116
type OfflineStoreDBStorePersistence struct {
117+
// Type of the persistence type you want to use. Allowed values are: snowflake.offline, bigquery, redshift, spark, postgres, trino, redis, athena, mssql
115118
// +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;trino;redis;athena;mssql
116119
Type string `json:"type"`
117120
// Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed.
@@ -146,7 +149,7 @@ type OnlineStorePersistence struct {
146149
DBPersistence *OnlineStoreDBStorePersistence `json:"store,omitempty"`
147150
}
148151

149-
// OnlineStoreFilePersistence configures the file-based persistence for the offline store service
152+
// OnlineStoreFilePersistence configures the file-based persistence for the online store service
150153
// +kubebuilder:validation:XValidation:rule="(!has(self.pvc) && has(self.path)) ? self.path.startsWith('/') : true",message="Ephemeral stores must have absolute paths."
151154
// +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."
152155
// +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."
@@ -155,8 +158,9 @@ type OnlineStoreFilePersistence struct {
155158
PvcConfig *PvcConfig `json:"pvc,omitempty"`
156159
}
157160

158-
// OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service
161+
// OnlineStoreDBStorePersistence configures the DB store persistence for the online store service
159162
type OnlineStoreDBStorePersistence struct {
163+
// Type of the persistence type you want to use. Allowed values are: snowflake.online, redis, ikv, datastore, dynamodb, bigtable, postgres, cassandra, mysql, hazelcast, singlestore, hbase, elasticsearch, qdrant, couchbase, milvus
160164
// +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase;milvus
161165
Type string `json:"type"`
162166
// Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed.
@@ -211,6 +215,7 @@ type RegistryFilePersistence struct {
211215

212216
// RegistryDBStorePersistence configures the DB store persistence for the registry service
213217
type RegistryDBStorePersistence struct {
218+
// Type of the persistence type you want to use. Allowed values are: sql, snowflake.registry
214219
// +kubebuilder:validation:Enum=sql;snowflake.registry
215220
Type string `json:"type"`
216221
// Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed.
@@ -289,6 +294,11 @@ type ServerConfigs struct {
289294
// Allowed values: "debug", "info", "warning", "error", "critical".
290295
// +kubebuilder:validation:Enum=debug;info;warning;error;critical
291296
LogLevel *string `json:"logLevel,omitempty"`
297+
// VolumeMounts defines the list of volumes that should be mounted into the feast container.
298+
// This allows attaching persistent storage, config files, secrets, or other resources
299+
// required by the Feast components. Ensure that each volume mount has a corresponding
300+
// volume definition in the Volumes field.
301+
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
292302
}
293303

294304
// ContainerConfigs k8s container settings for the server
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: postgres-secret
5+
namespace: default
6+
labels:
7+
app: postgres
8+
stringData:
9+
POSTGRES_DB: mydatabase
10+
POSTGRES_USER: admin
11+
POSTGRES_PASSWORD: password
12+
---
13+
apiVersion: v1
14+
kind: Secret
15+
metadata:
16+
name: feast-data-stores
17+
namespace: default
18+
stringData:
19+
sql: |
20+
path: postgresql+psycopg://admin:password@postgresql.default.svc.cluster.local:5432/mydatabase?sslmode=require&sslrootcert=/var/lib/postgresql/certs/ca.crt&sslcert=/var/lib/postgresql/certs/tls.crt&sslkey=/var/lib/postgresql/certs/tls.key
21+
cache_ttl_seconds: 60
22+
sqlalchemy_config_kwargs:
23+
echo: false
24+
pool_pre_ping: true
25+
postgres: |
26+
host: postgresql.default.svc.cluster.local
27+
port: 5432
28+
database: mydatabase
29+
db_schema: public
30+
user: admin
31+
password: password
32+
sslmode: require
33+
sslkey_path: /var/lib/postgresql/certs/tls.key
34+
sslcert_path: /var/lib/postgresql/certs/tls.crt
35+
sslrootcert_path: /var/lib/postgresql/certs/ca.crt
36+
---
37+
apiVersion: feast.dev/v1alpha1
38+
kind: FeatureStore
39+
metadata:
40+
name: example
41+
namespace: default
42+
spec:
43+
feastProject: postgres_tls_sample
44+
services:
45+
volumes:
46+
- name: postgres-certs
47+
secret:
48+
secretName: postgresql-client-certs
49+
items:
50+
- key: ca.crt
51+
path: ca.crt
52+
mode: 0644 # Readable by all, required by PostgreSQL
53+
- key: tls.crt
54+
path: tls.crt
55+
mode: 0644 # Required for the client certificate
56+
- key: tls.key
57+
path: tls.key
58+
mode: 0640 # Required for the private key
59+
offlineStore:
60+
volumeMounts:
61+
- name: postgres-certs
62+
mountPath: /var/lib/postgresql/certs
63+
readOnly: true
64+
persistence:
65+
store:
66+
type: postgres
67+
secretRef:
68+
name: feast-data-stores
69+
envFrom:
70+
- secretRef:
71+
name: postgres-secret
72+
onlineStore:
73+
volumeMounts:
74+
- name: postgres-certs
75+
mountPath: /var/lib/postgresql/certs
76+
readOnly: true
77+
persistence:
78+
store:
79+
type: postgres
80+
secretRef:
81+
name: feast-data-stores
82+
envFrom:
83+
- secretRef:
84+
name: postgres-secret
85+
registry:
86+
local:
87+
volumeMounts:
88+
- name: postgres-certs
89+
mountPath: /var/lib/postgresql/certs
90+
readOnly: true
91+
persistence:
92+
store:
93+
type: sql
94+
secretRef:
95+
name: feast-data-stores
96+
envFrom:
97+
- secretRef:
98+
name: postgres-secret
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
Copyright 2024 Feast Community.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
24+
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler"
25+
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/services"
26+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
27+
28+
feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1"
29+
. "github.com/onsi/ginkgo/v2"
30+
. "github.com/onsi/gomega"
31+
appsv1 "k8s.io/api/apps/v1"
32+
corev1 "k8s.io/api/core/v1"
33+
"k8s.io/apimachinery/pkg/api/errors"
34+
"k8s.io/apimachinery/pkg/types"
35+
)
36+
37+
var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts", func() {
38+
Context("When deploying featureStore Spec we should have an option do Volumes and VolumeMounts", func() {
39+
const resourceName = "services-ephemeral"
40+
const offlineType = "duckdb"
41+
var pullPolicy = corev1.PullAlways
42+
43+
ctx := context.Background()
44+
45+
typeNamespacedName := types.NamespacedName{
46+
Name: resourceName,
47+
Namespace: "default",
48+
}
49+
featurestore := &feastdevv1alpha1.FeatureStore{}
50+
onlineStorePath := "/data/online.db"
51+
registryPath := "/data/registry.db"
52+
53+
BeforeEach(func() {
54+
By("creating the custom resource for the Kind FeatureStore")
55+
err := k8sClient.Get(ctx, typeNamespacedName, featurestore)
56+
if err != nil && errors.IsNotFound(err) {
57+
resource := createFeatureStoreVolumeResource(resourceName, image, pullPolicy)
58+
resource.Spec.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{
59+
FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{
60+
Type: offlineType,
61+
},
62+
}
63+
resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{
64+
FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{
65+
Path: onlineStorePath,
66+
},
67+
}
68+
resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{
69+
Local: &feastdevv1alpha1.LocalRegistryConfig{
70+
Persistence: &feastdevv1alpha1.RegistryPersistence{
71+
FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{
72+
Path: registryPath,
73+
},
74+
},
75+
},
76+
}
77+
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
78+
}
79+
})
80+
AfterEach(func() {
81+
resource := &feastdevv1alpha1.FeatureStore{}
82+
err := k8sClient.Get(ctx, typeNamespacedName, resource)
83+
Expect(err).NotTo(HaveOccurred())
84+
85+
By("Cleanup the specific resource instance FeatureStore")
86+
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
87+
88+
})
89+
90+
It("should successfully reconcile the resource and volumes and volumeMounts should be available", func() {
91+
By("Reconciling the created resource")
92+
controllerReconciler := &FeatureStoreReconciler{
93+
Client: k8sClient,
94+
Scheme: k8sClient.Scheme(),
95+
}
96+
97+
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
98+
NamespacedName: typeNamespacedName,
99+
})
100+
Expect(err).NotTo(HaveOccurred())
101+
102+
resource := &feastdevv1alpha1.FeatureStore{}
103+
err = k8sClient.Get(ctx, typeNamespacedName, resource)
104+
Expect(err).NotTo(HaveOccurred())
105+
106+
feast := services.FeastServices{
107+
Handler: handler.FeastHandler{
108+
Client: controllerReconciler.Client,
109+
Context: ctx,
110+
Scheme: controllerReconciler.Scheme,
111+
FeatureStore: resource,
112+
},
113+
}
114+
115+
deploy := &appsv1.Deployment{}
116+
objMeta := feast.GetObjectMeta()
117+
err = k8sClient.Get(ctx, types.NamespacedName{
118+
Name: objMeta.Name,
119+
Namespace: objMeta.Namespace,
120+
}, deploy)
121+
122+
Expect(err).NotTo(HaveOccurred())
123+
124+
// Extract the PodSpec from DeploymentSpec
125+
podSpec := deploy.Spec.Template.Spec
126+
127+
// Validate Volumes
128+
// Validate Volumes - Check if our test volume exists among multiple
129+
Expect(podSpec.Volumes).To(ContainElement(WithTransform(func(v corev1.Volume) string {
130+
return v.Name
131+
}, Equal("test-volume"))), "Expected volume 'test-volume' to be present")
132+
133+
// Ensure 'online' container has the test volume mount
134+
var onlineContainer *corev1.Container
135+
for i, container := range podSpec.Containers {
136+
if container.Name == "online" {
137+
onlineContainer = &podSpec.Containers[i]
138+
break
139+
}
140+
}
141+
Expect(onlineContainer).ToNot(BeNil(), "Expected to find container 'online'")
142+
143+
// Validate that 'online' container has the test-volume mount
144+
Expect(onlineContainer.VolumeMounts).To(ContainElement(WithTransform(func(vm corev1.VolumeMount) string {
145+
return vm.Name
146+
}, Equal("test-volume"))), "Expected 'online' container to have volume mount 'test-volume'")
147+
148+
// Ensure all other containers do NOT have the test volume mount
149+
for _, container := range podSpec.Containers {
150+
if container.Name != "online" {
151+
Expect(container.VolumeMounts).ToNot(ContainElement(WithTransform(func(vm corev1.VolumeMount) string {
152+
return vm.Name
153+
}, Equal("test-volume"))), "Unexpected volume mount 'test-volume' found in container "+container.Name)
154+
}
155+
}
156+
157+
})
158+
})
159+
})
160+
161+
func createFeatureStoreVolumeResource(resourceName string, image string, pullPolicy corev1.PullPolicy) *feastdevv1alpha1.FeatureStore {
162+
volume := corev1.Volume{
163+
Name: "test-volume",
164+
VolumeSource: corev1.VolumeSource{
165+
EmptyDir: &corev1.EmptyDirVolumeSource{},
166+
},
167+
}
168+
volumeMount := corev1.VolumeMount{
169+
Name: "test-volume",
170+
MountPath: "/data",
171+
}
172+
173+
return &feastdevv1alpha1.FeatureStore{
174+
ObjectMeta: metav1.ObjectMeta{
175+
Name: resourceName,
176+
Namespace: "default",
177+
},
178+
Spec: feastdevv1alpha1.FeatureStoreSpec{
179+
FeastProject: feastProject,
180+
Services: &feastdevv1alpha1.FeatureStoreServices{
181+
Volumes: []corev1.Volume{volume},
182+
OfflineStore: &feastdevv1alpha1.OfflineStore{
183+
ServerConfigs: feastdevv1alpha1.ServerConfigs{
184+
ContainerConfigs: feastdevv1alpha1.ContainerConfigs{},
185+
},
186+
},
187+
OnlineStore: &feastdevv1alpha1.OnlineStore{
188+
ServerConfigs: feastdevv1alpha1.ServerConfigs{
189+
VolumeMounts: []corev1.VolumeMount{volumeMount},
190+
ContainerConfigs: feastdevv1alpha1.ContainerConfigs{
191+
DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{
192+
Image: &image,
193+
},
194+
OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{
195+
ImagePullPolicy: &pullPolicy,
196+
Resources: &corev1.ResourceRequirements{},
197+
},
198+
},
199+
},
200+
},
201+
UI: &feastdevv1alpha1.ServerConfigs{
202+
ContainerConfigs: feastdevv1alpha1.ContainerConfigs{
203+
DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{
204+
Image: &image,
205+
},
206+
OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{
207+
ImagePullPolicy: &pullPolicy,
208+
Resources: &corev1.ResourceRequirements{},
209+
},
210+
},
211+
},
212+
},
213+
},
214+
}
215+
}

0 commit comments

Comments
 (0)