diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index ee491c2e59..b281edcc52 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -105,6 +105,7 @@ type OnlineStorePersistence struct { // OnlineStoreFilePersistence configures the file-based persistence for the offline 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://')",message="Online store does not support S3 or GS buckets." type OnlineStoreFilePersistence struct { Path string `json:"path,omitempty"` PvcConfig *PvcConfig `json:"pvc,omitempty"` @@ -122,11 +123,15 @@ type RegistryPersistence struct { } // RegistryFilePersistence configures the file-based persistence for the registry 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('/') || self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="Ephemeral stores must use absolute paths or be S3 ('s3://') or GS ('gs://') object store URIs." // +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.pvc) && has(self.path)) ? !(self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="PVC persistence does not support S3 or GS object store URIs." +// +kubebuilder:validation:XValidation:rule="(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith('s3://') : true",message="Additional S3 settings are available only for S3 object store URIs." type RegistryFilePersistence struct { Path string `json:"path,omitempty"` PvcConfig *PvcConfig `json:"pvc,omitempty"` + // +optional + S3AddtlKwargs *map[string]string `json:"s3_additional_kwargs,omitempty"` } // PvcConfig defines the settings for a persistent file store based on PVCs. diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 59f44c406c..ef836bc80d 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -452,6 +452,17 @@ func (in *RegistryFilePersistence) DeepCopyInto(out *RegistryFilePersistence) { *out = new(PvcConfig) (*in).DeepCopyInto(*out) } + if in.S3AddtlKwargs != nil { + in, out := &in.S3AddtlKwargs, &out.S3AddtlKwargs + *out = new(map[string]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryFilePersistence. 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 5e470296a9..54d2783b75 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -545,6 +545,9 @@ spec: slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: Online store does not support S3 or GS buckets. + rule: has(self.path) && !self.path.startsWith('s3://') + && !self.path.startsWith('gs://') type: object resources: description: ResourceRequirements describes the compute resource @@ -816,15 +819,30 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object type: object x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + - message: Ephemeral stores must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) : true' - message: PVC path must be a file name only, with no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' type: object resources: description: ResourceRequirements describes the compute @@ -1427,6 +1445,10 @@ spec: no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: Online store does not support S3 or GS + buckets. + rule: has(self.path) && !self.path.startsWith('s3://') + && !self.path.startsWith('gs://') type: object resources: description: ResourceRequirements describes the compute @@ -1705,16 +1727,30 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object type: object x-kubernetes-validations: - - message: Ephemeral stores must have absolute - paths. + - message: Ephemeral stores must use absolute + paths or be S3 ('s3://') or GS ('gs://') object + store URIs. rule: '(!has(self.pvc) && has(self.path)) ? - self.path.startsWith(''/'') : true' + (self.path.startsWith(''/'') || self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' - message: PVC path must be a file name only, with no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: PVC persistence does not support S3 + or GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available + only for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' type: object resources: description: ResourceRequirements describes the compute diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml new file mode 100644 index 0000000000..4fc5221f4a --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml @@ -0,0 +1,24 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: sample-s3-registry +spec: + feastProject: my_project + services: + onlineStore: + persistence: + file: + path: /data/online_store.db + offlineStore: + persistence: + file: + type: dask + registry: + local: + persistence: + file: + path: s3://bucket/registry.db + s3_additional_kwargs: + ServerSideEncryption: AES256 + ACL: bucket-owner-full-control + CacheControl: max-age=3600 \ No newline at end of file diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 69d3df4534..f851aeb8c3 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -553,6 +553,9 @@ spec: slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: Online store does not support S3 or GS buckets. + rule: has(self.path) && !self.path.startsWith('s3://') + && !self.path.startsWith('gs://') type: object resources: description: ResourceRequirements describes the compute resource @@ -824,15 +827,30 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object type: object x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + - message: Ephemeral stores must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) : true' - message: PVC path must be a file name only, with no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' type: object resources: description: ResourceRequirements describes the compute @@ -1435,6 +1453,10 @@ spec: no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: Online store does not support S3 or GS + buckets. + rule: has(self.path) && !self.path.startsWith('s3://') + && !self.path.startsWith('gs://') type: object resources: description: ResourceRequirements describes the compute @@ -1713,16 +1735,30 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object type: object x-kubernetes-validations: - - message: Ephemeral stores must have absolute - paths. + - message: Ephemeral stores must use absolute + paths or be S3 ('s3://') or GS ('gs://') object + store URIs. rule: '(!has(self.pvc) && has(self.path)) ? - self.path.startsWith(''/'') : true' + (self.path.startsWith(''/'') || self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' - message: PVC path must be a file name only, with no slashes. rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') : true' + - message: PVC persistence does not support S3 + or GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available + only for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' type: object resources: description: ResourceRequirements describes the compute diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 7e8a448f19..41f37cef3c 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -67,6 +67,41 @@ func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feat return copy } +func onlineStoreWithS3BucketForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + Path: "s3://bucket/online_store.db", + PvcConfig: &feastdevv1alpha1.PvcConfig{ + Create: &feastdevv1alpha1.PvcCreate{}, + MountPath: "/data/online", + }, + }, + }, + }, + } + return copy +} +func onlineStoreWithGsBucketForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + Path: "gs://bucket/online_store.db", + PvcConfig: &feastdevv1alpha1.PvcConfig{ + Create: &feastdevv1alpha1.PvcCreate{}, + MountPath: "/data/online", + }, + }, + }, + }, + } + return copy +} + func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { copy := featureStore.DeepCopy() copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ @@ -111,6 +146,77 @@ func registryWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feature } return copy } +func registryWithS3BucketForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + Path: "s3://bucket/registry.db", + PvcConfig: &feastdevv1alpha1.PvcConfig{ + Create: &feastdevv1alpha1.PvcCreate{}, + MountPath: "/data/registry", + }, + }, + }, + }, + }, + } + return copy +} +func registryWithGsBucketForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + Path: "gs://bucket/registry.db", + PvcConfig: &feastdevv1alpha1.PvcConfig{ + Create: &feastdevv1alpha1.PvcCreate{}, + MountPath: "/data/registry", + }, + }, + }, + }, + }, + } + return copy +} +func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + Path: "/data/online_store.db", + S3AddtlKwargs: &map[string]string{}, + }, + }, + }, + }, + } + return copy +} +func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + copy := featureStore.DeepCopy() + copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + Path: "gs://online_store.db", + S3AddtlKwargs: &map[string]string{}, + }, + }, + }, + }, + } + return copy +} + func pvcConfigWithNeitherRefNorCreate(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { copy := featureStore.DeepCopy() copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ @@ -239,6 +345,12 @@ var _ = Describe("FeatureStore API", func() { It("should fail when ephemeral persistence has relative path", func() { attemptInvalidCreationAndAsserts(ctx, onlineStoreWithRelativePathForEphemeral(featurestore), "Ephemeral stores must have absolute paths") }) + It("should fail when PVC persistence has S3 bucket", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithS3BucketForPvc(featurestore), "Online store does not support S3 or GS") + }) + It("should fail when PVC persistence has GS bucket", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithGsBucketForPvc(featurestore), "Online store does not support S3 or GS") + }) }) Context("When creating an invalid Offline Store", func() { @@ -256,7 +368,15 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithAbsolutePathForPvc(featurestore), "PVC path must be a file name only") }) It("should fail when ephemeral persistence has relative path", func() { - attemptInvalidCreationAndAsserts(ctx, registryWithRelativePathForEphemeral(featurestore), "Ephemeral stores must have absolute paths") + attemptInvalidCreationAndAsserts(ctx, registryWithRelativePathForEphemeral(featurestore), "Ephemeral stores must use absolute paths or be S3 ('s3://') or GS ('gs://')") + }) + It("should fail when PVC persistence has S3 bucket", func() { + attemptInvalidCreationAndAsserts(ctx, registryWithS3BucketForPvc(featurestore), "PVC persistence does not support S3 or GS object store URIs") + attemptInvalidCreationAndAsserts(ctx, registryWithGsBucketForPvc(featurestore), "PVC persistence does not support S3 or GS object store URIs") + }) + It("should fail when additional S3 settings are provided to non S3 bucket", func() { + attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForFile(featurestore), "Additional S3 settings are available only for S3 object store URIs") + attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) })