From 664fdcaaacb2a2e4f3d01a5fb19c6672fee58b58 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 08:57:20 +0100 Subject: [PATCH 01/24] feat: add per-service scaling and grpc support Signed-off-by: Jatin Kumar --- .../api/v1/featurestore_types.go | 59 + .../api/v1/zz_generated.deepcopy.go | 153 + .../crd/bases/feast.dev_featurestores.yaml | 3020 ++++++++++------- .../config/samples/kustomization.yaml | 1 + .../samples/v1_featurestore_mcp_grpc.yaml | 21 + .../controller/featurestore_controller.go | 23 +- .../featurestore_controller_test.go | 2 +- .../internal/controller/services/cronjob.go | 20 +- .../controller/services/repo_config.go | 6 + .../internal/controller/services/services.go | 259 +- .../controller/services/services_test.go | 24 +- .../controller/services/services_types.go | 22 + .../internal/controller/services/tls_test.go | 6 +- .../internal/controller/services/util.go | 20 + 14 files changed, 2393 insertions(+), 1243 deletions(-) create mode 100644 infra/feast-operator/config/samples/v1_featurestore_mcp_grpc.yaml diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index c64c26ce020..18505aa03d2 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -33,6 +33,7 @@ const ( ClientReadyType = "Client" OfflineStoreReadyType = "OfflineStore" OnlineStoreReadyType = "OnlineStore" + OnlineStoreGrpcReadyType = "OnlineStoreGrpc" RegistryReadyType = "Registry" UIReadyType = "UI" ReadyType = "FeatureStore" @@ -45,6 +46,7 @@ const ( DeploymentNotAvailableReason = "DeploymentNotAvailable" OfflineStoreFailedReason = "OfflineStoreDeploymentFailed" OnlineStoreFailedReason = "OnlineStoreDeploymentFailed" + OnlineStoreGrpcFailedReason = "OnlineStoreGrpcDeploymentFailed" RegistryFailedReason = "RegistryDeploymentFailed" UIFailedReason = "UIDeploymentFailed" ClientFailedReason = "ClientDeploymentFailed" @@ -55,6 +57,7 @@ const ( ReadyMessage = "FeatureStore installation complete" OfflineStoreReadyMessage = "Offline Store installation complete" OnlineStoreReadyMessage = "Online Store installation complete" + OnlineStoreGrpcReadyMessage = "Online Store gRPC installation complete" RegistryReadyMessage = "Registry installation complete" UIReadyMessage = "UI installation complete" ClientReadyMessage = "Client installation complete" @@ -73,10 +76,46 @@ type FeatureStoreSpec struct { FeastProject string `json:"feastProject"` FeastProjectDir *FeastProjectDir `json:"feastProjectDir,omitempty"` Services *FeatureStoreServices `json:"services,omitempty"` + // FeatureServer configures the Feast feature server, including MCP support. + FeatureServer *FeatureServerConfig `json:"feature_server,omitempty"` AuthzConfig *AuthzConfig `json:"authz,omitempty"` CronJob *FeastCronJob `json:"cronJob,omitempty"` } +// FeatureServerConfig defines feature server configuration settings. +// Fields are aligned with Feast's feature_store.yaml feature_server schema. +type FeatureServerConfig struct { + // Feature server type selector (e.g. local, mcp) + Type *string `json:"type,omitempty" yaml:"type,omitempty"` + // Whether the feature server should be launched. + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + // Enable MCP server support - defaults to false. + MCPEnabled *bool `json:"mcp_enabled,omitempty" yaml:"mcp_enabled,omitempty"` + // MCP server name for identification. + MCPServerName *string `json:"mcp_server_name,omitempty" yaml:"mcp_server_name,omitempty"` + // MCP server version. + MCPServerVersion *string `json:"mcp_server_version,omitempty" yaml:"mcp_server_version,omitempty"` + // Optional MCP transport configuration. + MCPTransport *string `json:"mcp_transport,omitempty" yaml:"mcp_transport,omitempty"` + // The endpoint definition for transformation_service. + TransformationServiceEndpoint *string `json:"transformation_service_endpoint,omitempty" yaml:"transformation_service_endpoint,omitempty"` + // Feature logging configuration. + FeatureLogging *FeatureLoggingConfig `json:"feature_logging,omitempty" yaml:"feature_logging,omitempty"` +} + +// FeatureLoggingConfig defines feature server logging settings. +type FeatureLoggingConfig struct { + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + // Interval of flushing logs to the destination in offline store. + FlushIntervalSecs *int32 `json:"flush_interval_secs,omitempty" yaml:"flush_interval_secs,omitempty"` + // Interval of dumping logs collected in memory to local disk. + WriteToDiskIntervalSecs *int32 `json:"write_to_disk_interval_secs,omitempty" yaml:"write_to_disk_interval_secs,omitempty"` + // Log queue capacity. + QueueCapacity *int32 `json:"queue_capacity,omitempty" yaml:"queue_capacity,omitempty"` + // Timeout for adding new log item to the queue. + EmitTimeoutMicroSecs *int32 `json:"emit_timeout_micro_secs,omitempty" yaml:"emit_timeout_micro_secs,omitempty"` +} + // FeastProjectDir defines how to create the feast project directory. // +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init)].exists_one(c, c)",message="One selection required between init or git." type FeastProjectDir struct { @@ -349,6 +388,8 @@ var ValidOfflineStoreDBStorePersistenceTypes = []string{ type OnlineStore struct { // Creates a feature server container Server *ServerConfigs `json:"server,omitempty"` + // Creates a gRPC feature server container (feast listen) + Grpc *GrpcServerConfigs `json:"grpc,omitempty"` Persistence *OnlineStorePersistence `json:"persistence,omitempty"` } @@ -512,6 +553,8 @@ type FeatureStoreRef struct { type ServerConfigs struct { ContainerConfigs `json:",inline"` TLS *TlsConfigs `json:"tls,omitempty"` + // Replicas sets the number of replicas for the service deployment. + Replicas *int32 `json:"replicas,omitempty"` // LogLevel sets the logging level for the server // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical @@ -563,6 +606,21 @@ type WorkerConfigs struct { RegistryTTLSeconds *int32 `json:"registryTTLSeconds,omitempty"` } +// GrpcServerConfigs creates a gRPC feature server for the online store. +type GrpcServerConfigs struct { + ContainerConfigs `json:",inline"` + // Replicas sets the number of replicas for the gRPC service deployment. + Replicas *int32 `json:"replicas,omitempty"` + // VolumeMounts defines the list of volumes that should be mounted into the gRPC container. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + // Port sets the gRPC server port. Defaults to 50051 if unset. + Port *int32 `json:"port,omitempty"` + // MaxWorkers sets the maximum number of threads for handling gRPC calls. + MaxWorkers *int32 `json:"maxWorkers,omitempty"` + // RegistryTTLSeconds sets how often the registry is refreshed. + RegistryTTLSeconds *int32 `json:"registryTTLSeconds,omitempty"` +} + // RegistryServerConfigs creates a registry server for the feast service, with specified container configurations. // +kubebuilder:validation:XValidation:rule="self.restAPI == true || self.grpc == true || !has(self.grpc)", message="At least one of restAPI or grpc must be true" type RegistryServerConfigs struct { @@ -686,6 +744,7 @@ type FeatureStoreStatus struct { type ServiceHostnames struct { OfflineStore string `json:"offlineStore,omitempty"` OnlineStore string `json:"onlineStore,omitempty"` + OnlineStoreGrpc string `json:"onlineStoreGrpc,omitempty"` Registry string `json:"registry,omitempty"` RegistryRest string `json:"registryRest,omitempty"` UI string `json:"ui,omitempty"` diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index 9e328bf5b6c..5abd433c033 100644 --- a/infra/feast-operator/api/v1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -208,6 +208,101 @@ func (in *FeastProjectDir) DeepCopy() *FeastProjectDir { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureLoggingConfig) DeepCopyInto(out *FeatureLoggingConfig) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.FlushIntervalSecs != nil { + in, out := &in.FlushIntervalSecs, &out.FlushIntervalSecs + *out = new(int32) + **out = **in + } + if in.WriteToDiskIntervalSecs != nil { + in, out := &in.WriteToDiskIntervalSecs, &out.WriteToDiskIntervalSecs + *out = new(int32) + **out = **in + } + if in.QueueCapacity != nil { + in, out := &in.QueueCapacity, &out.QueueCapacity + *out = new(int32) + **out = **in + } + if in.EmitTimeoutMicroSecs != nil { + in, out := &in.EmitTimeoutMicroSecs, &out.EmitTimeoutMicroSecs + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureLoggingConfig. +func (in *FeatureLoggingConfig) DeepCopy() *FeatureLoggingConfig { + if in == nil { + return nil + } + out := new(FeatureLoggingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureServerConfig) DeepCopyInto(out *FeatureServerConfig) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.MCPEnabled != nil { + in, out := &in.MCPEnabled, &out.MCPEnabled + *out = new(bool) + **out = **in + } + if in.MCPServerName != nil { + in, out := &in.MCPServerName, &out.MCPServerName + *out = new(string) + **out = **in + } + if in.MCPServerVersion != nil { + in, out := &in.MCPServerVersion, &out.MCPServerVersion + *out = new(string) + **out = **in + } + if in.MCPTransport != nil { + in, out := &in.MCPTransport, &out.MCPTransport + *out = new(string) + **out = **in + } + if in.TransformationServiceEndpoint != nil { + in, out := &in.TransformationServiceEndpoint, &out.TransformationServiceEndpoint + *out = new(string) + **out = **in + } + if in.FeatureLogging != nil { + in, out := &in.FeatureLogging, &out.FeatureLogging + *out = new(FeatureLoggingConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureServerConfig. +func (in *FeatureServerConfig) DeepCopy() *FeatureServerConfig { + if in == nil { + return nil + } + out := new(FeatureServerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeatureStore) DeepCopyInto(out *FeatureStore) { *out = *in @@ -347,6 +442,11 @@ func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { *out = new(FeatureStoreServices) (*in).DeepCopyInto(*out) } + if in.FeatureServer != nil { + in, out := &in.FeatureServer, &out.FeatureServer + *out = new(FeatureServerConfig) + (*in).DeepCopyInto(*out) + } if in.AuthzConfig != nil { in, out := &in.AuthzConfig, &out.AuthzConfig *out = new(AuthzConfig) @@ -437,6 +537,49 @@ func (in *GitCloneOptions) DeepCopy() *GitCloneOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrpcServerConfigs) DeepCopyInto(out *GrpcServerConfigs) { + *out = *in + in.ContainerConfigs.DeepCopyInto(&out.ContainerConfigs) + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + 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]) + } + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } + if in.MaxWorkers != nil { + in, out := &in.MaxWorkers, &out.MaxWorkers + *out = new(int32) + **out = **in + } + if in.RegistryTTLSeconds != nil { + in, out := &in.RegistryTTLSeconds, &out.RegistryTTLSeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrpcServerConfigs. +func (in *GrpcServerConfigs) DeepCopy() *GrpcServerConfigs { + if in == nil { + return nil + } + out := new(GrpcServerConfigs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobSpec) DeepCopyInto(out *JobSpec) { *out = *in @@ -669,6 +812,11 @@ func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = new(ServerConfigs) (*in).DeepCopyInto(*out) } + if in.Grpc != nil { + in, out := &in.Grpc, &out.Grpc + *out = new(GrpcServerConfigs) + (*in).DeepCopyInto(*out) + } if in.Persistence != nil { in, out := &in.Persistence, &out.Persistence *out = new(OnlineStorePersistence) @@ -1043,6 +1191,11 @@ func (in *ServerConfigs) DeepCopyInto(out *ServerConfigs) { *out = new(TlsConfigs) (*in).DeepCopyInto(*out) } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } if in.LogLevel != nil { in, out := &in.LogLevel, &out.LogLevel *out = new(string) 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 9ef46ebbb32..add32ae00e4 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -691,6 +691,56 @@ spec: x-kubernetes-validations: - message: One selection required between init or git. rule: '[has(self.git), has(self.init)].exists_one(c, c)' + feature_server: + description: FeatureServer configures the Feast feature server, including + MCP support. + properties: + enabled: + description: Whether the feature server should be launched. + type: boolean + feature_logging: + description: Feature logging configuration. + properties: + emit_timeout_micro_secs: + description: Timeout for adding new log item to the queue. + format: int32 + type: integer + enabled: + type: boolean + flush_interval_secs: + description: Interval of flushing logs to the destination + in offline store. + format: int32 + type: integer + queue_capacity: + description: Log queue capacity. + format: int32 + type: integer + write_to_disk_interval_secs: + description: Interval of dumping logs collected in memory + to local disk. + format: int32 + type: integer + type: object + mcp_enabled: + description: Enable MCP server support - defaults to false. + type: boolean + mcp_server_name: + description: MCP server name for identification. + type: string + mcp_server_version: + description: MCP server version. + type: string + mcp_transport: + description: Optional MCP transport configuration. + type: string + transformation_service_endpoint: + description: The endpoint definition for transformation_service. + type: string + type: + description: Feature server type selector (e.g. local, mcp) + type: string + type: object services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -1049,6 +1099,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -1228,158 +1283,9 @@ spec: onlineStore: description: OnlineStore configures the online store service properties: - persistence: - description: OnlineStorePersistence configures the persistence - settings for the online store service - properties: - file: - description: OnlineStoreFilePersistence configures the - file-based persistence for the online store service - properties: - path: - type: string - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object - type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath - type: object - x-kubernetes-validations: - - message: One selection is required between ref and - create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and must - not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: object - x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') - : 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: Online store does not support S3 or GS buckets. - rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' - store: - description: OnlineStoreDBStorePersistence configures - the DB store persistence for the online store service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the secret - key. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you want - to use. - enum: - - snowflake.online - - redis - - ikv - - datastore - - dynamodb - - bigtable - - postgres - - cassandra - - mysql - - hazelcast - - singlestore - - hbase - - elasticsearch - - qdrant - - couchbase.online - - milvus - - hybrid - type: string - required: - - secretRef - - type - type: object - type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, c)' - server: - description: Creates a feature server container + grpc: + description: Creates a gRPC feature server container (feast + listen) properties: env: items: @@ -1540,25 +1446,30 @@ spec: description: PullPolicy describes a policy for if/when to pull a container image type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean + maxWorkers: + description: MaxWorkers sets the maximum number of threads + for handling gRPC calls. + format: int32 + type: integer nodeSelector: additionalProperties: type: string type: object + port: + description: Port sets the gRPC server port. Defaults + to 50051 if unset. + format: int32 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds sets how often the registry + is refreshed. + format: int32 + type: integer + replicas: + description: Replicas sets the number of replicas for + the gRPC service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -1605,47 +1516,9 @@ spec: of compute resources required. type: object type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, where - TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key - names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' volumeMounts: description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + that should be mounted into the gRPC container. items: description: VolumeMount describes a mounting of a Volume within a container. @@ -1687,199 +1560,72 @@ spec: - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. + type: object + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * - CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object - type: object - type: object - registry: - description: Registry configures the registry service. One selection - is required. Local is the default setting. - properties: - local: - description: LocalRegistryConfig configures the registry service - properties: - persistence: - description: RegistryPersistence configures the persistence - settings for the registry service - properties: - file: - description: RegistryFilePersistence configures the - file-based persistence for the registry service + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. properties: - cache_mode: - description: |- - CacheMode defines the registry cache update strategy. - Allowed values are "sync" and "thread". - enum: - - none - - sync - - thread - type: string - cache_ttl_seconds: - description: CacheTTLSeconds defines the TTL (in - seconds) for the registry cache. - format: int32 - minimum: 0 - type: integer - path: - type: string - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. + create: + description: Settings for creating a new PVC properties: - create: - description: Settings for creating a new PVC + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath type: object x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - 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: Registry files 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' - store: - description: RegistryDBStorePersistence configures - the DB store persistence for the registry service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + ref: + description: Reference to an existing field properties: name: default: "" @@ -1890,146 +1636,109 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - sql - - snowflake.registry - type: string required: - - secretRef - - type + - mountPath type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') type: object x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a registry server container + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : 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: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service properties: - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: - supports metadata.name, metadata.namespace, - `metadata.labels['''']`, `metadata.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source - of a set of ConfigMaps + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - configMapRef: - description: The ConfigMap to select from + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: + key: + description: The key to select. + type: string name: default: "" description: |- @@ -2039,17 +1748,62 @@ spec: type: string optional: description: Specify whether the ConfigMap - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string name: default: "" description: |- @@ -2059,256 +1813,159 @@ spec: type: string optional: description: Specify whether the Secret - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic type: object - type: array - grpc: - description: Enable gRPC registry server. Defaults - to true if unset. - type: boolean - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - restAPI: - description: Enable REST API registry server. - type: boolean - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of - a Volume within a container. + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string name: - description: This must match the Name of a Volume. - type: string - readOnly: + default: "" description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined type: boolean - recursiveReadOnly: + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - subPath: + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume - from which the container's volume should be - mounted. + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string required: - - mountPath - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object type: object - x-kubernetes-validations: - - message: At least one of restAPI or grpc must be true - rule: self.restAPI == true || self.grpc == true || !has(self.grpc) - type: object - remote: - description: RemoteRegistryConfig points to a remote feast - registry server. - properties: - feastRef: - description: Reference to an existing `FeatureStore` CR - in the same k8s cluster. - properties: - name: - description: Name of the FeatureStore - type: string - namespace: - description: Namespace of the FeatureStore - type: string - required: - - name - type: object - hostname: - description: Host address of the remote registry service - - :, e.g. `registry..svc.cluster.local:80` - type: string tls: - description: TlsRemoteRegistryConfigs configures client - TLS for a remote feast registry. + description: TlsConfigs configures server TLS for a feast + service. properties: - certName: - description: defines the configmap key name for the - client TLS cert. - type: string - configMapRef: - description: references the local k8s configmap where - the TLS cert resides + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -2319,68 +1976,754 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - certName - - configMapRef + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer type: object type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, c)' - securityContext: - description: PodSecurityContext holds pod-level security attributes - and common container settings. + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. properties: - appArmorProfile: - description: appArmorProfile is the AppArmor options to use - by the containers in this pod. - properties: - localhostProfile: - description: localhostProfile indicates a profile loaded - on the node that should be used. - type: string - type: - description: type indicates which kind of AppArmor profile - will be applied. - type: string - required: - - type - type: object - fsGroup: - description: A special supplemental group that applies to - all containers in a pod. - format: int64 - type: integer - fsGroupChangePolicy: - description: |- - fsGroupChangePolicy defines behavior of changing ownership and permission of the volume - before being exposed inside Pod. - type: string - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. + local: + description: LocalRegistryConfig configures the registry service properties: - level: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - 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: Registry files 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' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: description: Level is SELinux level label that applies to the container. type: string @@ -2643,6 +2986,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for the + service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -4824,229 +5172,572 @@ spec: description: The ConfigMap to select from properties: name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - ikv + - couchbase + - clickhouse + type: string + type: object + type: object + x-kubernetes-validations: + - message: One selection required between init or git. + rule: '[has(self.git), has(self.init)].exists_one(c, c)' + feature_server: + description: FeatureServer configures the Feast feature server, + including MCP support. + properties: + enabled: + description: Whether the feature server should be launched. + type: boolean + feature_logging: + description: Feature logging configuration. + properties: + emit_timeout_micro_secs: + description: Timeout for adding new log item to the queue. + format: int32 + type: integer + enabled: + type: boolean + flush_interval_secs: + description: Interval of flushing logs to the destination + in offline store. + format: int32 + type: integer + queue_capacity: + description: Log queue capacity. + format: int32 + type: integer + write_to_disk_interval_secs: + description: Interval of dumping logs collected in memory + to local disk. + format: int32 + type: integer + type: object + mcp_enabled: + description: Enable MCP server support - defaults to false. + type: boolean + mcp_server_name: + description: MCP server name for identification. + type: string + mcp_server_version: + description: MCP server version. + type: string + mcp_transport: + description: Optional MCP transport configuration. + type: string + transformation_service_endpoint: + description: The endpoint definition for transformation_service. + type: string + type: + description: Feature server type selector (e.g. local, mcp) + type: string + type: object + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be + unavailable during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service + properties: + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" + value: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - featureRepoPath: - description: FeatureRepoPath is the relative path to the - feature repo subdirectory. Default is 'feature_repo'. - type: string - ref: - description: Reference to a branch / tag / commit - type: string - url: - description: The repository URL to clone from. - type: string - required: - - url - type: object - x-kubernetes-validations: - - message: RepoPath must be a file name only, with no slashes. - rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') - : true' - init: - description: FeastInitOptions defines how to run a `feast - init`. - properties: - minimal: - type: boolean - template: - description: Template for the created project - enum: - - local - - gcp - - aws - - snowflake - - spark - - postgres - - hbase - - cassandra - - hazelcast - - ikv - - couchbase - - clickhouse - type: string - type: object - type: object - x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. - properties: - deploymentStrategy: - description: DeploymentStrategy describes how to replace existing - pods with new ones. - properties: - rollingUpdate: - description: |- - Rolling update config params. Present only if DeploymentStrategyType = - RollingUpdate. - properties: - maxSurge: - anyOf: - - type: integer - - type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: description: |- - The maximum number of pods that can be scheduled above the desired number of - pods. - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: The maximum number of pods that can be - unavailable during the update. - x-kubernetes-int-or-string: true - type: object - type: - description: Type of deployment. Can be "Recreate" or - "RollingUpdate". Default is RollingUpdate. - type: string - type: object - disableInitContainers: - description: Disable the 'feast repo initialization' initContainer - type: boolean - offlineStore: - description: OfflineStore configures the offline store service - properties: - persistence: - description: OfflineStorePersistence configures the persistence - settings for the offline store service - properties: - file: - description: OfflineStoreFilePersistence configures - the file-based persistence for the offline store - service + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. properties: - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object - type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object - x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and - must not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: - enum: - - file - - dask - - duckdb - type: string type: object - store: - description: OfflineStoreDBStorePersistence configures - the DB store persistence for the offline store service + tls: + description: TlsConfigs configures server TLS for + a feast service. properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -5057,33 +5748,110 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - snowflake.offline - - bigquery - - redshift - - spark - - postgres - - trino - - athena - - mssql - - couchbase.offline - - clickhouse - - ray - type: string - required: - - secretRef - - type + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer type: object type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a remote offline server container + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + grpc: + description: Creates a gRPC feature server container (feast + listen) properties: env: items: @@ -5247,25 +6015,30 @@ spec: description: PullPolicy describes a policy for if/when to pull a container image type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean + maxWorkers: + description: MaxWorkers sets the maximum number of + threads for handling gRPC calls. + format: int32 + type: integer nodeSelector: additionalProperties: type: string type: object + port: + description: Port sets the gRPC server port. Defaults + to 50051 if unset. + format: int32 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds sets how often the + registry is refreshed. + format: int32 + type: integer + replicas: + description: Replicas sets the number of replicas + for the gRPC service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -5312,47 +6085,9 @@ spec: of compute resources required. type: object type: object - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' volumeMounts: description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + that should be mounted into the gRPC container. items: description: VolumeMount describes a mounting of a Volume within a container. @@ -5395,57 +6130,7 @@ spec: - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object type: object - type: object - onlineStore: - description: OnlineStore configures the online store service - properties: persistence: description: OnlineStorePersistence configures the persistence settings for the online store service @@ -5784,6 +6469,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -6331,6 +7021,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -6890,6 +7585,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -8569,6 +9269,8 @@ spec: type: string onlineStore: type: string + onlineStoreGrpc: + type: string registry: type: string registryRest: diff --git a/infra/feast-operator/config/samples/kustomization.yaml b/infra/feast-operator/config/samples/kustomization.yaml index 127bd5894b4..342b35bcaf5 100644 --- a/infra/feast-operator/config/samples/kustomization.yaml +++ b/infra/feast-operator/config/samples/kustomization.yaml @@ -3,4 +3,5 @@ resources: - v1_featurestore.yaml - v1_featurestore_with_ui.yaml - v1_featurestore_all_remote_servers.yaml +- v1_featurestore_mcp_grpc.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/infra/feast-operator/config/samples/v1_featurestore_mcp_grpc.yaml b/infra/feast-operator/config/samples/v1_featurestore_mcp_grpc.yaml new file mode 100644 index 00000000000..c14f9314930 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_mcp_grpc.yaml @@ -0,0 +1,21 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-mcp-grpc +spec: + feastProject: my_project + feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_server_name: "feast-feature-store" + mcp_server_version: "1.0.0" + feature_logging: + enabled: false + services: + onlineStore: + server: + replicas: 2 + grpc: + replicas: 1 + port: 50051 diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index a9591d97c8a..f4d56918b65 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -179,17 +179,19 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Message: "Error: " + err.Error(), } } else { - deployment, deploymentErr := feast.GetDeployment() - if deploymentErr != nil { - condition = metav1.Condition{ - Type: feastdevv1.ReadyType, - Status: metav1.ConditionUnknown, - Reason: feastdevv1.DeploymentNotAvailableReason, - Message: feastdevv1.DeploymentNotAvailableMessage, - } + for _, feastType := range feast.GetDeploymentTypes() { + deployment, deploymentErr := feast.GetDeployment(feastType) + if deploymentErr != nil { + condition = metav1.Condition{ + Type: feastdevv1.ReadyType, + Status: metav1.ConditionUnknown, + Reason: feastdevv1.DeploymentNotAvailableReason, + Message: feastdevv1.DeploymentNotAvailableMessage, + } - result = errResult - } else { + result = errResult + break + } isDeployAvailable := services.IsDeploymentAvailable(deployment.Status.Conditions) if !isDeployAvailable { condition = metav1.Condition{ @@ -200,6 +202,7 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 } result = errResult + break } } } diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index bfd4a484cff..5816531c0bd 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -134,7 +134,7 @@ var _ = Describe("FeatureStore Controller", func() { }, } - deployment, _ := feast.GetDeployment() + deployment, _ := feast.GetDeployment(services.OnlineFeastType) deployment.Status = appsv1.DeploymentStatus{ Conditions: []appsv1.DeploymentCondition{ { diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index f3b978928f7..7738afa4ad9 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -171,7 +171,7 @@ func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string containerName, "", []string{ - "kubectl", "exec", "deploy/" + feast.initFeastDeploy().Name, "-i", + "kubectl", "exec", "deploy/" + feast.getCronJobTargetDeploymentName(), "-i", "--", "bash", "-c", cronJobCmd, }, @@ -180,6 +180,22 @@ func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string ) } +func (feast *FeastServices) getCronJobTargetDeploymentName() string { + if feast.isOnlineServer() { + return feast.initFeastDeploy(OnlineFeastType).Name + } + if feast.isRegistryServer() { + return feast.initFeastDeploy(RegistryFeastType).Name + } + if feast.isOfflineServer() { + return feast.initFeastDeploy(OfflineFeastType).Name + } + if feast.isOnlineGrpcServer() { + return feast.initFeastDeploy(OnlineGrpcFeastType).Name + } + return feast.initFeastDeploy(OnlineFeastType).Name +} + func (feast *FeastServices) createCronJobRole() error { logger := log.FromContext(feast.Handler.Context) role := feast.initCronJobRole() @@ -209,7 +225,7 @@ func (feast *FeastServices) setCronJobRole(role *rbacv1.Role) error { { APIGroups: []string{appsv1.GroupName}, Resources: []string{"deployments"}, - ResourceNames: []string{feast.initFeastDeploy().Name}, + ResourceNames: []string{feast.getCronJobTargetDeploymentName()}, Verbs: []string{"get"}, }, { diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 00c09563fd9..491998fb650 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -86,6 +86,9 @@ func getBaseServiceRepoConfig( secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { repoConfig := defaultRepoConfig(featureStore) + if featureStore.Status.Applied.FeatureServer != nil { + repoConfig.FeatureServer = featureStore.Status.Applied.FeatureServer + } clientRepoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc, nil) if err != nil { return repoConfig, err @@ -311,6 +314,9 @@ func getRepoConfig( secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { status := featureStore.Status repoConfig := initRepoConfig(status.Applied.FeastProject) + if status.Applied.FeatureServer != nil { + repoConfig.FeatureServer = status.Applied.FeatureServer + } if status.Applied.AuthzConfig != nil { if status.Applied.AuthzConfig.KubernetesAuthz != nil { repoConfig.AuthzConfig = AuthzConfig{ diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 6771e9498af..b219fec1a31 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -18,6 +18,7 @@ package services import ( "errors" + "fmt" "strconv" "strings" @@ -102,6 +103,16 @@ func (feast *FeastServices) Deploy() error { } } + if feast.isOnlineGrpcServer() { + if err = feast.deployFeastServiceByType(OnlineGrpcFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(OnlineGrpcFeastType); err != nil { + return err + } + } + if feast.isLocalRegistry() { err := feast.validateRegistryPersistence(services.Registry.Local.Persistence) if err != nil { @@ -135,9 +146,6 @@ func (feast *FeastServices) Deploy() error { if err := feast.createServiceAccount(); err != nil { return err } - if err := feast.createDeployment(); err != nil { - return err - } if err := feast.deployClient(); err != nil { return err } @@ -221,6 +229,13 @@ func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence * } func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) error { + hasServerConfig := false + if feastType == OnlineGrpcFeastType { + hasServerConfig = feast.isOnlineGrpcServer() + } else if feast.getServerConfigs(feastType) != nil { + hasServerConfig = true + } + if pvcCreate, shouldCreate := shouldCreatePvc(feast.Handler.FeatureStore, feastType); shouldCreate { if err := feast.createPVC(pvcCreate, feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) @@ -228,7 +243,7 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) } else { _ = feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)) } - if serviceConfig := feast.getServerConfigs(feastType); serviceConfig != nil { + if hasServerConfig { // For registry service, handle both gRPC and REST services if feastType == RegistryFeastType && feast.isRegistryServer() { // Create gRPC service if enabled @@ -266,6 +281,9 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) return feast.setFeastServiceCondition(err, feastType) } } + if err := feast.createDeploymentForType(feastType); err != nil { + return feast.setFeastServiceCondition(err, feastType) + } } else { _ = feast.Handler.DeleteOwnedFeastObj(feast.initFeastSvc(feastType)) // Delete REST API service if it exists @@ -283,6 +301,9 @@ func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastSvc(feastType)); err != nil { return err } + if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastDeploy(feastType)); err != nil { + return err + } if err := feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { return err } @@ -327,11 +348,11 @@ func (feast *FeastServices) createServiceAccount() error { return nil } -func (feast *FeastServices) createDeployment() error { +func (feast *FeastServices) createDeploymentForType(feastType FeastServiceType) error { logger := log.FromContext(feast.Handler.Context) - deploy := feast.initFeastDeploy() + deploy := feast.initFeastDeploy(feastType) if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, deploy, controllerutil.MutateFn(func() error { - return feast.setDeployment(deploy) + return feast.setDeployment(deploy, feastType) })); err != nil { return err } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { @@ -379,11 +400,14 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1.PvcCreate, feastType return nil } -func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { +func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType FeastServiceType) error { cr := feast.Handler.FeatureStore replicas := deploy.Spec.Replicas - deploy.Labels = feast.getLabels() + deploy.Labels = feast.getFeastTypeLabels(feastType) + if desiredReplicas := feast.getReplicasForType(feastType); desiredReplicas != nil { + replicas = desiredReplicas + } deploy.Spec = appsv1.DeploymentSpec{ Replicas: replicas, Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), @@ -398,48 +422,75 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { }, }, } - if err := feast.setPod(&deploy.Spec.Template.Spec); err != nil { + if err := feast.setPod(&deploy.Spec.Template.Spec, feastType); err != nil { return err } return controllerutil.SetControllerReference(cr, deploy, feast.Handler.Scheme) } -func (feast *FeastServices) setPod(podSpec *corev1.PodSpec) error { - if err := feast.setContainers(podSpec); err != nil { +func (feast *FeastServices) setPod(podSpec *corev1.PodSpec, feastType FeastServiceType) error { + if err := feast.setContainers(podSpec, feastType); err != nil { return err } feast.mountTlsConfigs(podSpec) - feast.mountPvcConfigs(podSpec) + feast.mountPvcConfigs(podSpec, feastType) feast.mountEmptyDirVolumes(podSpec) feast.mountUserDefinedVolumes(podSpec) - feast.applyNodeSelector(podSpec) + feast.applyNodeSelector(podSpec, feastType) return nil } -func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { +func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec, feastType FeastServiceType) error { fsYamlB64, err := feast.GetServiceFeatureStoreYamlBase64() if err != nil { return err } feast.setInitContainer(podSpec, fsYamlB64) - if feast.isRegistryServer() { - feast.setContainer(&podSpec.Containers, RegistryFeastType, fsYamlB64) - } - if feast.isOnlineServer() { - feast.setContainer(&podSpec.Containers, OnlineFeastType, fsYamlB64) - } - if feast.isOfflineServer() { - feast.setContainer(&podSpec.Containers, OfflineFeastType, fsYamlB64) - } - if feast.isUiServer() { - feast.setContainer(&podSpec.Containers, UIFeastType, fsYamlB64) - } + feast.setContainer(&podSpec.Containers, feastType, fsYamlB64) return nil } func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastType FeastServiceType, fsYamlB64 string) { + if feastType == OnlineGrpcFeastType { + grpcCfg := feast.getOnlineGrpcConfigs() + if grpcCfg == nil { + return + } + name := string(feastType) + workingDir := feast.getFeatureRepoDir() + cmd := feast.getGrpcContainerCommand() + container := getContainer(name, workingDir, cmd, grpcCfg.ContainerConfigs, fsYamlB64) + container.Ports = []corev1.ContainerPort{ + { + Name: "grpc", + ContainerPort: feast.getOnlineGrpcPort(), + Protocol: corev1.ProtocolTCP, + }, + } + probeHandler := feast.getProbeHandler(feastType, &feastdevv1.TlsConfigs{}) + container.StartupProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 3, + FailureThreshold: 40, + } + container.LivenessProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 20, + FailureThreshold: 6, + } + container.ReadinessProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 10, + } + volumeMounts := feast.getVolumeMounts(feastType) + if len(volumeMounts) > 0 { + container.VolumeMounts = append(container.VolumeMounts, volumeMounts...) + } + *containers = append(*containers, *container) + return + } if serverConfigs := feast.getServerConfigs(feastType); serverConfigs != nil { name := string(feastType) workingDir := feast.getFeatureRepoDir() @@ -532,6 +583,12 @@ func (feast *FeastServices) mountUserDefinedVolumes(podSpec *corev1.PodSpec) { } func (feast *FeastServices) getVolumeMounts(feastType FeastServiceType) (volumeMounts []corev1.VolumeMount) { + if feastType == OnlineGrpcFeastType { + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil { + return grpcCfg.VolumeMounts + } + return []corev1.VolumeMount{} + } if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.VolumeMounts } @@ -631,6 +688,20 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st return feastCommand } +func (feast *FeastServices) getGrpcContainerCommand() []string { + address := fmt.Sprintf("0.0.0.0:%d", feast.getOnlineGrpcPort()) + cmd := []string{"feast", "listen", "--address", address} + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil { + if grpcCfg.MaxWorkers != nil { + cmd = append(cmd, "--max_workers", strconv.Itoa(int(*grpcCfg.MaxWorkers))) + } + if grpcCfg.RegistryTTLSeconds != nil { + cmd = append(cmd, "--registry_ttl_sec", strconv.Itoa(int(*grpcCfg.RegistryTTLSeconds))) + } + } + return cmd +} + func (feast *FeastServices) getDeploymentStrategy() appsv1.DeploymentStrategy { if feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy != nil { return *feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy @@ -702,6 +773,22 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType, isRestService bool) error { svc.Labels = feast.getFeastTypeLabels(feastType) + if feastType == OnlineGrpcFeastType { + grpcPort := feast.getOnlineGrpcPort() + svc.Spec = corev1.ServiceSpec{ + Selector: feast.getFeastTypeLabels(feastType), + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "grpc", + Port: grpcPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(grpcPort)), + }, + }, + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, svc, feast.Handler.Scheme) + } if feast.isOpenShiftTls(feastType) { if len(svc.Annotations) == 0 { svc.Annotations = map[string]string{} @@ -757,7 +844,7 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi } svc.Spec = corev1.ServiceSpec{ - Selector: feast.getLabels(), + Selector: feast.getFeastTypeLabels(feastType), Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { @@ -829,6 +916,8 @@ func (feast *FeastServices) getServerConfigs(feastType FeastServiceType) *feastd if feast.isOnlineStore() { return appliedServices.OnlineStore.Server } + case OnlineGrpcFeastType: + return nil case RegistryFeastType: if feast.isRegistryServer() { return &appliedServices.Registry.Local.Server.ServerConfigs @@ -839,6 +928,21 @@ func (feast *FeastServices) getServerConfigs(feastType FeastServiceType) *feastd return nil } +func (feast *FeastServices) getOnlineGrpcConfigs() *feastdevv1.GrpcServerConfigs { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + if appliedServices != nil && appliedServices.OnlineStore != nil { + return appliedServices.OnlineStore.Grpc + } + return nil +} + +func (feast *FeastServices) getOnlineGrpcPort() int32 { + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil && grpcCfg.Port != nil { + return *grpcCfg.Port + } + return DefaultOnlineGrpcPort +} + func (feast *FeastServices) getLogLevelForType(feastType FeastServiceType) *string { if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.LogLevel @@ -853,6 +957,19 @@ func (feast *FeastServices) getWorkerConfigs(feastType FeastServiceType) *feastd return nil } +func (feast *FeastServices) getReplicasForType(feastType FeastServiceType) *int32 { + if feastType == OnlineGrpcFeastType { + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil { + return grpcCfg.Replicas + } + return nil + } + if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { + return serviceConfigs.Replicas + } + return nil +} + func (feast *FeastServices) isMetricsEnabled(feastType FeastServiceType) bool { if feastType != OnlineFeastType { return false @@ -866,34 +983,26 @@ func (feast *FeastServices) isMetricsEnabled(feastType FeastServiceType) bool { } func (feast *FeastServices) getNodeSelectorForType(feastType FeastServiceType) *map[string]string { + if feastType == OnlineGrpcFeastType { + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil { + return grpcCfg.ContainerConfigs.OptionalCtrConfigs.NodeSelector + } + return nil + } if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.ContainerConfigs.OptionalCtrConfigs.NodeSelector } return nil } -func (feast *FeastServices) applyNodeSelector(podSpec *corev1.PodSpec) { - // Merge node selectors from all services - mergedNodeSelector := make(map[string]string) - - // Check all service types for node selector configuration - allServiceTypes := append(feastServerTypes, UIFeastType) - for _, feastType := range allServiceTypes { - if selector := feast.getNodeSelectorForType(feastType); selector != nil && len(*selector) > 0 { - for k, v := range *selector { - mergedNodeSelector[k] = v - } - } - } - - // If no service has node selector configured, we're done - if len(mergedNodeSelector) == 0 { +func (feast *FeastServices) applyNodeSelector(podSpec *corev1.PodSpec, feastType FeastServiceType) { + selector := feast.getNodeSelectorForType(feastType) + if selector == nil || len(*selector) == 0 { return } // Merge with any existing node selectors (from ops team or other sources) - // This preserves pre-existing selectors while adding operator requirements - finalNodeSelector := feast.mergeNodeSelectors(podSpec.NodeSelector, mergedNodeSelector) + finalNodeSelector := feast.mergeNodeSelectors(podSpec.NodeSelector, *selector) podSpec.NodeSelector = finalNodeSelector } @@ -929,9 +1038,9 @@ func (feast *FeastServices) GetFeastServiceName(feastType FeastServiceType) stri return GetFeastServiceName(feast.Handler.FeatureStore, feastType) } -func (feast *FeastServices) GetDeployment() (appsv1.Deployment, error) { +func (feast *FeastServices) GetDeployment(feastType FeastServiceType) (appsv1.Deployment, error) { deployment := appsv1.Deployment{} - obj := feast.GetObjectMeta() + obj := feast.GetObjectMetaType(feastType) err := feast.Handler.Get(feast.Handler.Context, client.ObjectKey{Namespace: obj.GetNamespace(), Name: obj.GetName()}, &deployment) return deployment, err } @@ -970,6 +1079,12 @@ func (feast *FeastServices) setServiceHostnames() error { feast.Handler.FeatureStore.Status.ServiceHostnames.OnlineStore = objMeta.Name + "." + objMeta.Namespace + domain + getPortStr(feast.Handler.FeatureStore.Status.Applied.Services.OnlineStore.Server.TLS) } + if feast.isOnlineGrpcServer() { + objMeta := feast.initFeastSvc(OnlineGrpcFeastType) + grpcPort := feast.getOnlineGrpcPort() + feast.Handler.FeatureStore.Status.ServiceHostnames.OnlineStoreGrpc = objMeta.Name + "." + objMeta.Namespace + domain + + strconv.Itoa(int(grpcPort)) + } if feast.isRegistryServer() { objMeta := feast.initFeastSvc(RegistryFeastType) feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = objMeta.Name + "." + objMeta.Namespace + domain + @@ -1098,13 +1213,18 @@ func (feast *FeastServices) isOnlineServer() bool { feast.Handler.FeatureStore.Status.Applied.Services.OnlineStore.Server != nil } +func (feast *FeastServices) isOnlineGrpcServer() bool { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.OnlineStore != nil && appliedServices.OnlineStore.Grpc != nil +} + func (feast *FeastServices) isOnlineStore() bool { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services return appliedServices != nil && appliedServices.OnlineStore != nil } func (feast *FeastServices) noLocalCoreServerConfigured() bool { - return !(feast.isRegistryServer() || feast.isOnlineServer() || feast.isOfflineServer()) + return !(feast.isRegistryServer() || feast.isOnlineServer() || feast.isOnlineGrpcServer() || feast.isOfflineServer()) } func (feast *FeastServices) isUiServer() bool { @@ -1112,9 +1232,29 @@ func (feast *FeastServices) isUiServer() bool { return appliedServices != nil && appliedServices.UI != nil } -func (feast *FeastServices) initFeastDeploy() *appsv1.Deployment { +func (feast *FeastServices) GetDeploymentTypes() []FeastServiceType { + deployments := []FeastServiceType{} + if feast.isOfflineServer() { + deployments = append(deployments, OfflineFeastType) + } + if feast.isOnlineServer() { + deployments = append(deployments, OnlineFeastType) + } + if feast.isOnlineGrpcServer() { + deployments = append(deployments, OnlineGrpcFeastType) + } + if feast.isRegistryServer() { + deployments = append(deployments, RegistryFeastType) + } + if feast.isUiServer() { + deployments = append(deployments, UIFeastType) + } + return deployments +} + +func (feast *FeastServices) initFeastDeploy(feastType FeastServiceType) *appsv1.Deployment { deploy := &appsv1.Deployment{ - ObjectMeta: feast.GetObjectMeta(), + ObjectMeta: feast.GetObjectMetaType(feastType), } deploy.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) return deploy @@ -1183,11 +1323,9 @@ func applyCtrConfigs(container *corev1.Container, containerConfigs feastdevv1.Co } } -func (feast *FeastServices) mountPvcConfigs(podSpec *corev1.PodSpec) { - for _, feastType := range feastServerTypes { - if pvcConfig, hasPvcConfig := hasPvcConfig(feast.Handler.FeatureStore, feastType); hasPvcConfig { - feast.mountPvcConfig(podSpec, pvcConfig, feastType) - } +func (feast *FeastServices) mountPvcConfigs(podSpec *corev1.PodSpec, feastType FeastServiceType) { + if pvcConfig, hasPvcConfig := hasPvcConfig(feast.Handler.FeatureStore, feastType); hasPvcConfig { + feast.mountPvcConfig(podSpec, pvcConfig, feastType) } } @@ -1279,6 +1417,15 @@ func getTargetRestPort(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) i func (feast *FeastServices) getProbeHandler(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) corev1.ProbeHandler { targetPort := getTargetPort(feastType, tls) + if feastType == OnlineGrpcFeastType { + targetPort = feast.getOnlineGrpcPort() + return corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(int(targetPort)), + }, + } + } + if feastType == RegistryFeastType { if feast.isRegistryGrpcEnabled() { return corev1.ProbeHandler{ diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index b8863e10a74..ebf298e87ed 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -160,7 +160,7 @@ var _ = Describe("Registry Service", func() { It("should configure correct gRPC container ports", func() { setFeatureStoreServerConfig(true, false) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -173,7 +173,7 @@ var _ = Describe("Registry Service", func() { It("should configure correct REST container ports", func() { setFeatureStoreServerConfig(false, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -192,7 +192,7 @@ var _ = Describe("Registry Service", func() { setFeatureStoreServerConfig(true, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -219,7 +219,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify NodeSelector is applied - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -263,7 +263,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify merged NodeSelector is applied - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -298,7 +298,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment first - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -363,7 +363,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify UI service selector is applied - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -387,7 +387,7 @@ var _ = Describe("Registry Service", func() { Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -428,7 +428,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify no NodeSelector is applied (empty selector) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -472,7 +472,7 @@ var _ = Describe("Registry Service", func() { Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -520,7 +520,7 @@ var _ = Describe("Registry Service", func() { Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -559,7 +559,7 @@ var _ = Describe("Registry Service", func() { Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) Expect(feast.setDeployment(deployment)).To(Succeed()) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index d437c703a6a..b3a3c247d51 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -55,10 +55,12 @@ const ( DefaultOnlineStorageRequest = "5Gi" DefaultRegistryStorageRequest = "5Gi" MetricsPort int32 = 8000 + DefaultOnlineGrpcPort int32 = 50051 AuthzFeastType FeastServiceType = "authorization" OfflineFeastType FeastServiceType = "offline" OnlineFeastType FeastServiceType = "online" + OnlineGrpcFeastType FeastServiceType = "online-grpc" RegistryFeastType FeastServiceType = "registry" UIFeastType FeastServiceType = "ui" ClientFeastType FeastServiceType = "client" @@ -113,6 +115,11 @@ var ( TargetHttpPort: 6566, TargetHttpsPort: 6567, }, + OnlineGrpcFeastType: { + Args: []string{"listen"}, + TargetHttpPort: DefaultOnlineGrpcPort, + TargetHttpsPort: DefaultOnlineGrpcPort, + }, RegistryFeastType: { Args: []string{"serve_registry"}, TargetHttpPort: 6570, @@ -154,6 +161,19 @@ var ( Reason: feastdevv1.OnlineStoreFailedReason, }, }, + OnlineGrpcFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1.OnlineStoreGrpcReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.OnlineStoreGrpcReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1.OnlineStoreGrpcReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1.OnlineStoreGrpcFailedReason, + }, + }, RegistryFeastType: { metav1.ConditionTrue: { Type: feastdevv1.RegistryReadyType, @@ -218,6 +238,7 @@ var feastServerTypes = []FeastServiceType{ RegistryFeastType, OfflineFeastType, OnlineFeastType, + OnlineGrpcFeastType, } // AuthzType defines the authorization type @@ -255,6 +276,7 @@ type RepoConfig struct { OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` Registry RegistryConfig `yaml:"registry,omitempty"` AuthzConfig AuthzConfig `yaml:"auth,omitempty"` + FeatureServer *feastdevv1.FeatureServerConfig `yaml:"feature_server,omitempty"` EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` } diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index e5299d79119..b699a5d26a7 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -170,7 +170,7 @@ var _ = Describe("TLS Config", func() { Expect(openshiftTls).To(BeTrue()) // check k8s deployment objects - feastDeploy := feast.initFeastDeploy() + feastDeploy := feast.initFeastDeploy(OnlineFeastType) err = feast.setDeployment(feastDeploy) Expect(err).ToNot(HaveOccurred()) Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) @@ -320,7 +320,7 @@ var _ = Describe("TLS Config", func() { Expect(uiSvc.Spec.Ports[0].Name).To(Equal(HttpScheme)) // check k8s deployment objects - feastDeploy = feast.initFeastDeploy() + feastDeploy = feast.initFeastDeploy(OnlineFeastType) err = feast.setDeployment(feastDeploy) Expect(err).ToNot(HaveOccurred()) Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(4)) @@ -370,7 +370,7 @@ var _ = Describe("TLS Config", func() { Expect(registryRestSvc.Annotations).NotTo(BeEmpty()) Expect(registryRestSvc.Spec.Ports[0].Name).To(Equal(HttpsScheme)) - feastDeploy = feast.initFeastDeploy() + feastDeploy = feast.initFeastDeploy(OnlineFeastType) err = feast.setDeployment(feastDeploy) Expect(err).ToNot(HaveOccurred()) registryContainer := GetRegistryContainer(*feastDeploy) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 8e8a717aecf..345bf48cddc 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -46,6 +46,11 @@ func hasPvcConfig(featureStore *feastdevv1.FeatureStore, feastType FeastServiceT services.OnlineStore.Persistence.FilePersistence != nil { pvcConfig = services.OnlineStore.Persistence.FilePersistence.PvcConfig } + case OnlineGrpcFeastType: + if services.OnlineStore != nil && services.OnlineStore.Persistence != nil && + services.OnlineStore.Persistence.FilePersistence != nil { + pvcConfig = services.OnlineStore.Persistence.FilePersistence.PvcConfig + } case OfflineFeastType: if services.OfflineStore != nil && services.OfflineStore.Persistence != nil && services.OfflineStore.Persistence.FilePersistence != nil { @@ -183,6 +188,21 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { services.OnlineStore.Server = &feastdevv1.ServerConfigs{} } setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) + if services.OnlineStore.Grpc != nil { + setDefaultCtrConfigs(&services.OnlineStore.Grpc.ContainerConfigs.DefaultCtrConfigs) + if services.OnlineStore.Grpc.Port == nil { + defaultPort := DefaultOnlineGrpcPort + services.OnlineStore.Grpc.Port = &defaultPort + } + if services.OnlineStore.Grpc.MaxWorkers == nil { + defaultMaxWorkers := int32(10) + services.OnlineStore.Grpc.MaxWorkers = &defaultMaxWorkers + } + if services.OnlineStore.Grpc.RegistryTTLSeconds == nil { + defaultRegistryTTL := int32(5) + services.OnlineStore.Grpc.RegistryTTLSeconds = &defaultRegistryTTL + } + } if services.UI != nil { setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs) From fe4a6f1b828f57624a3c77aa938062a91add6680 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 09:15:38 +0100 Subject: [PATCH 02/24] fix: avoid default http when grpc only Signed-off-by: Jatin Kumar --- infra/feast-operator/internal/controller/services/util.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 345bf48cddc..51cfa2d445b 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -184,10 +184,12 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) } - if services.OnlineStore.Server == nil { + if services.OnlineStore.Server == nil && services.OnlineStore.Grpc == nil { services.OnlineStore.Server = &feastdevv1.ServerConfigs{} } - setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) + if services.OnlineStore.Server != nil { + setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) + } if services.OnlineStore.Grpc != nil { setDefaultCtrConfigs(&services.OnlineStore.Grpc.ContainerConfigs.DefaultCtrConfigs) if services.OnlineStore.Grpc.Port == nil { From 246fa44fc08cf6c739efa9ffc766462bed908bc3 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 09:27:14 +0100 Subject: [PATCH 03/24] test: update operator tests for per-service deployments Signed-off-by: Jatin Kumar --- .../controller/services/services_test.go | 49 ++++++++-------- .../internal/controller/services/tls_test.go | 58 +++++++++++-------- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index ebf298e87ed..fb384775b76 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -160,9 +160,9 @@ var _ = Describe("Registry Service", func() { It("should configure correct gRPC container ports", func() { setFeatureStoreServerConfig(true, false) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(RegistryFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, RegistryFeastType)).To(Succeed()) ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(1)) @@ -173,9 +173,9 @@ var _ = Describe("Registry Service", func() { It("should configure correct REST container ports", func() { setFeatureStoreServerConfig(false, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(RegistryFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, RegistryFeastType)).To(Succeed()) ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(1)) @@ -192,9 +192,9 @@ var _ = Describe("Registry Service", func() { setFeatureStoreServerConfig(true, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(RegistryFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, RegistryFeastType)).To(Succeed()) ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(2)) @@ -219,9 +219,9 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify NodeSelector is applied - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(RegistryFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, RegistryFeastType)).To(Succeed()) // Verify NodeSelector is applied to pod spec expectedNodeSelector := map[string]string{ @@ -231,7 +231,7 @@ var _ = Describe("Registry Service", func() { Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) - It("should merge NodeSelectors from multiple services", func() { + It("should apply online NodeSelector when multiple services configured", func() { // Set NodeSelector for registry service registryNodeSelector := map[string]string{ "kubernetes.io/os": "linux", @@ -265,11 +265,10 @@ var _ = Describe("Registry Service", func() { // Create deployment and verify merged NodeSelector is applied deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) - // Verify NodeSelector merges all service selectors (online overrides registry for node-type) + // Verify NodeSelector uses online service selector only expectedNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", "node-type": "online", "zone": "us-west-1a", } @@ -298,9 +297,9 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment first - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(UIFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, UIFeastType)).To(Succeed()) // Simulate a mutating webhook or admission controller adding node selectors // This would happen after the operator creates the pod spec but before scheduling @@ -312,7 +311,7 @@ var _ = Describe("Registry Service", func() { // Apply the node selector logic again to test merging // This simulates the operator reconciling and re-applying node selectors - feast.applyNodeSelector(&deployment.Spec.Template.Spec) + feast.applyNodeSelector(&deployment.Spec.Template.Spec, UIFeastType) // Verify NodeSelector merges existing and operator selectors expectedNodeSelector := map[string]string{ @@ -323,7 +322,7 @@ var _ = Describe("Registry Service", func() { Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) - It("should apply UI service NodeSelector when UI has highest precedence", func() { + It("should apply UI NodeSelector for UI deployment", func() { // Set NodeSelector for online service onlineNodeSelector := map[string]string{ "node-type": "online", @@ -363,11 +362,11 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify UI service selector is applied - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(UIFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, UIFeastType)).To(Succeed()) - // Verify NodeSelector is applied with UI service's selector (UI wins) + // Verify NodeSelector is applied with UI service's selector expectedNodeSelector := map[string]string{ "node-type": "ui", "zone": "us-east-1", @@ -389,7 +388,7 @@ var _ = Describe("Registry Service", func() { deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) @@ -428,9 +427,9 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify no NodeSelector is applied (empty selector) - deployment := feast.initFeastDeploy(OnlineFeastType) + deployment := feast.initFeastDeploy(RegistryFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, RegistryFeastType)).To(Succeed()) // Verify no NodeSelector is applied (empty selector) Expect(deployment.Spec.Template.Spec.NodeSelector).To(BeEmpty()) @@ -474,7 +473,7 @@ var _ = Describe("Registry Service", func() { deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) @@ -522,7 +521,7 @@ var _ = Describe("Registry Service", func() { deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) @@ -561,7 +560,7 @@ var _ = Describe("Registry Service", func() { deployment := feast.initFeastDeploy(OnlineFeastType) Expect(deployment).NotTo(BeNil()) - Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index b699a5d26a7..00a956b9f81 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -171,15 +171,13 @@ var _ = Describe("TLS Config", func() { // check k8s deployment objects feastDeploy := feast.initFeastDeploy(OnlineFeastType) - err = feast.setDeployment(feastDeploy) + err = feast.setDeployment(feastDeploy, OnlineFeastType) Expect(err).ToNot(HaveOccurred()) Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - Expect(feastDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) - Expect(feastDeploy.Spec.Template.Spec.Containers[1].Command).To(ContainElements(ContainSubstring("--key"))) - Expect(feastDeploy.Spec.Template.Spec.Containers[2].Command).To(ContainElements(ContainSubstring("--key"))) - Expect(feastDeploy.Spec.Template.Spec.Containers[3].Command).To(ContainElements(ContainSubstring("--key"))) - Expect(feastDeploy.Spec.Template.Spec.Volumes).To(HaveLen(5)) + Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + onlineContainer := GetOnlineContainer(*feastDeploy) + Expect(onlineContainer).NotTo(BeNil()) + Expect(onlineContainer.Command).To(ContainElements(ContainSubstring("--key"))) // registry service w/ tls and in an openshift cluster feast.Handler.FeatureStore = minimalFeatureStore() @@ -320,21 +318,33 @@ var _ = Describe("TLS Config", func() { Expect(uiSvc.Spec.Ports[0].Name).To(Equal(HttpScheme)) // check k8s deployment objects - feastDeploy = feast.initFeastDeploy(OnlineFeastType) - err = feast.setDeployment(feastDeploy) + offlineDeploy := feast.initFeastDeploy(OfflineFeastType) + err = feast.setDeployment(offlineDeploy, OfflineFeastType) Expect(err).ToNot(HaveOccurred()) - Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - Expect(GetOfflineContainer(*feastDeploy)).NotTo(BeNil()) - Expect(feastDeploy.Spec.Template.Spec.Volumes).To(HaveLen(2)) - - Expect(GetRegistryContainer(*feastDeploy).Command).NotTo(ContainElements(ContainSubstring("--key"))) - Expect(GetRegistryContainer(*feastDeploy).VolumeMounts).To(HaveLen(1)) - Expect(GetOfflineContainer(*feastDeploy).Command).To(ContainElements(ContainSubstring("--key"))) - Expect(GetOfflineContainer(*feastDeploy).VolumeMounts).To(HaveLen(2)) - Expect(GetOnlineContainer(*feastDeploy).Command).NotTo(ContainElements(ContainSubstring("--key"))) - Expect(GetOnlineContainer(*feastDeploy).VolumeMounts).To(HaveLen(1)) - Expect(GetUIContainer(*feastDeploy).Command).NotTo(ContainElements(ContainSubstring("--key"))) - Expect(GetUIContainer(*feastDeploy).VolumeMounts).To(HaveLen(1)) + offlineContainer := GetOfflineContainer(*offlineDeploy) + Expect(offlineContainer).NotTo(BeNil()) + Expect(offlineContainer.Command).To(ContainElements(ContainSubstring("--key"))) + + onlineDeploy := feast.initFeastDeploy(OnlineFeastType) + err = feast.setDeployment(onlineDeploy, OnlineFeastType) + Expect(err).ToNot(HaveOccurred()) + onlineContainer = GetOnlineContainer(*onlineDeploy) + Expect(onlineContainer).NotTo(BeNil()) + Expect(onlineContainer.Command).NotTo(ContainElements(ContainSubstring("--key"))) + + registryDeploy := feast.initFeastDeploy(RegistryFeastType) + err = feast.setDeployment(registryDeploy, RegistryFeastType) + Expect(err).ToNot(HaveOccurred()) + registryContainer := GetRegistryContainer(*registryDeploy) + Expect(registryContainer).NotTo(BeNil()) + Expect(registryContainer.Command).NotTo(ContainElements(ContainSubstring("--key"))) + + uiDeploy := feast.initFeastDeploy(UIFeastType) + err = feast.setDeployment(uiDeploy, UIFeastType) + Expect(err).ToNot(HaveOccurred()) + uiContainer := GetUIContainer(*uiDeploy) + Expect(uiContainer).NotTo(BeNil()) + Expect(uiContainer.Command).NotTo(ContainElements(ContainSubstring("--key"))) // Test REST registry server TLS configuration feast.Handler.FeatureStore = minimalFeatureStore() @@ -370,10 +380,10 @@ var _ = Describe("TLS Config", func() { Expect(registryRestSvc.Annotations).NotTo(BeEmpty()) Expect(registryRestSvc.Spec.Ports[0].Name).To(Equal(HttpsScheme)) - feastDeploy = feast.initFeastDeploy(OnlineFeastType) - err = feast.setDeployment(feastDeploy) + feastDeploy = feast.initFeastDeploy(RegistryFeastType) + err = feast.setDeployment(feastDeploy, RegistryFeastType) Expect(err).ToNot(HaveOccurred()) - registryContainer := GetRegistryContainer(*feastDeploy) + registryContainer = GetRegistryContainer(*feastDeploy) Expect(registryContainer).NotTo(BeNil()) Expect(registryContainer.Command).To(ContainElements(ContainSubstring("--key"))) }) From 564654558f4399ee624de875b7fea2656f0b36ff Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 09:40:09 +0100 Subject: [PATCH 04/24] refactor: reduce complexity in defaults and deploy Signed-off-by: Jatin Kumar --- .../internal/controller/services/services.go | 131 ++++++++-------- .../internal/controller/services/util.go | 141 +++++++++++------- 2 files changed, 155 insertions(+), 117 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index b219fec1a31..9a952d93ddf 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -52,97 +52,106 @@ func (feast *FeastServices) ApplyDefaults() error { // Deploy the feast services func (feast *FeastServices) Deploy() error { + if err := feast.validateLocalServers(); err != nil { + return err + } + if err := feast.reconcileOpenshiftTls(); err != nil { + return err + } + if err := feast.reconcileOffline(); err != nil { + return err + } + if err := feast.reconcileOnline(); err != nil { + return err + } + if err := feast.reconcileOnlineGrpc(); err != nil { + return err + } + if err := feast.reconcileRegistry(); err != nil { + return err + } + if err := feast.reconcileUI(); err != nil { + return err + } + return feast.deploySupportServices() +} + +func (feast *FeastServices) validateLocalServers() error { if feast.noLocalCoreServerConfigured() { return errors.New("at least one local server must be configured. e.g. registry / online / offline") } - if feast.isRegistryServer() { - if !feast.isRegistryGrpcEnabled() && !feast.isRegistryRestEnabled() { - return errors.New("at least one of gRPC or REST API must be enabled for registry service") - } + if feast.isRegistryServer() && !feast.isRegistryGrpcEnabled() && !feast.isRegistryRestEnabled() { + return errors.New("at least one of gRPC or REST API must be enabled for registry service") } + return nil +} + +func (feast *FeastServices) reconcileOpenshiftTls() error { openshiftTls, err := feast.checkOpenshiftTls() if err != nil { return err } if openshiftTls { - if err := feast.createCaConfigMap(); err != nil { - return err - } - } else { - _ = feast.Handler.DeleteOwnedFeastObj(feast.initCaConfigMap()) + return feast.createCaConfigMap() } + _ = feast.Handler.DeleteOwnedFeastObj(feast.initCaConfigMap()) + return nil +} - services := feast.Handler.FeatureStore.Status.Applied.Services +func (feast *FeastServices) reconcileOffline() error { if feast.isOfflineStore() { - err := feast.validateOfflineStorePersistence(services.OfflineStore.Persistence) - if err != nil { - return err - } - - if err = feast.deployFeastServiceByType(OfflineFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OfflineFeastType); err != nil { + services := feast.Handler.FeatureStore.Status.Applied.Services + if err := feast.validateOfflineStorePersistence(services.OfflineStore.Persistence); err != nil { return err } + return feast.deployFeastServiceByType(OfflineFeastType) } + return feast.removeFeastServiceByType(OfflineFeastType) +} +func (feast *FeastServices) reconcileOnline() error { if feast.isOnlineStore() { - err := feast.validateOnlineStorePersistence(services.OnlineStore.Persistence) - if err != nil { - return err - } - - if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OnlineFeastType); err != nil { + services := feast.Handler.FeatureStore.Status.Applied.Services + if err := feast.validateOnlineStorePersistence(services.OnlineStore.Persistence); err != nil { return err } + return feast.deployFeastServiceByType(OnlineFeastType) } + return feast.removeFeastServiceByType(OnlineFeastType) +} +func (feast *FeastServices) reconcileOnlineGrpc() error { if feast.isOnlineGrpcServer() { - if err = feast.deployFeastServiceByType(OnlineGrpcFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OnlineGrpcFeastType); err != nil { - return err - } + return feast.deployFeastServiceByType(OnlineGrpcFeastType) } + return feast.removeFeastServiceByType(OnlineGrpcFeastType) +} +func (feast *FeastServices) reconcileRegistry() error { if feast.isLocalRegistry() { - err := feast.validateRegistryPersistence(services.Registry.Local.Persistence) - if err != nil { - return err - } - - if err = feast.deployFeastServiceByType(RegistryFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(RegistryFeastType); err != nil { + services := feast.Handler.FeatureStore.Status.Applied.Services + if err := feast.validateRegistryPersistence(services.Registry.Local.Persistence); err != nil { return err } + return feast.deployFeastServiceByType(RegistryFeastType) } + return feast.removeFeastServiceByType(RegistryFeastType) +} + +func (feast *FeastServices) reconcileUI() error { if feast.isUiServer() { - if err = feast.deployFeastServiceByType(UIFeastType); err != nil { - return err - } - if err = feast.createRoute(UIFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(UIFeastType); err != nil { - return err - } - if err := feast.removeRoute(UIFeastType); err != nil { + if err := feast.deployFeastServiceByType(UIFeastType); err != nil { return err } + return feast.createRoute(UIFeastType) + } + if err := feast.removeFeastServiceByType(UIFeastType); err != nil { + return err } + return feast.removeRoute(UIFeastType) +} +func (feast *FeastServices) deploySupportServices() error { if err := feast.createServiceAccount(); err != nil { return err } @@ -152,11 +161,7 @@ func (feast *FeastServices) Deploy() error { if err := feast.deployNamespaceRegistry(); err != nil { return err } - if err := feast.deployCronJob(); err != nil { - return err - } - - return nil + return feast.deployCronJob() } func (feast *FeastServices) validateRegistryPersistence(registryPersistence *feastdevv1.RegistryPersistence) error { diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 51cfa2d445b..e98449f4321 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -95,75 +95,100 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { cr.Status.FeastVersion = feastversion.FeastVersion applied := &cr.Status.Applied + ensureProjectDirDefaults(applied) + services := ensureServiceDefaults(applied) + applyRegistryDefaults(cr, services) + applyOfflineDefaults(services) + applyOnlineDefaults(cr, services) + applyUiDefaults(services) + applyCronJobDefaults(applied) +} + +func ensureProjectDirDefaults(applied *feastdevv1.FeatureStoreSpec) { if applied.FeastProjectDir == nil { applied.FeastProjectDir = &feastdevv1.FeastProjectDir{ Init: &feastdevv1.FeastInitOptions{}, } } +} + +func ensureServiceDefaults(applied *feastdevv1.FeatureStoreSpec) *feastdevv1.FeatureStoreServices { if applied.Services == nil { applied.Services = &feastdevv1.FeatureStoreServices{} } - services := applied.Services + return applied.Services +} - if services.Registry != nil { - // if remote registry not set, proceed w/ local registry defaults - if services.Registry.Remote == nil { - // if local registry not set, apply an empty pointer struct - if services.Registry.Local == nil { - services.Registry.Local = &feastdevv1.LocalRegistryConfig{} - } - if services.Registry.Local.Persistence == nil { - services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{} - } +func applyRegistryDefaults(cr *feastdevv1.FeatureStore, services *feastdevv1.FeatureStoreServices) { + if services.Registry == nil { + return + } - if services.Registry.Local.Persistence.DBPersistence == nil { - if services.Registry.Local.Persistence.FilePersistence == nil { - services.Registry.Local.Persistence.FilePersistence = &feastdevv1.RegistryFilePersistence{} - } + // if remote registry not set, proceed w/ local registry defaults + if services.Registry.Remote == nil { + // if local registry not set, apply an empty pointer struct + if services.Registry.Local == nil { + services.Registry.Local = &feastdevv1.LocalRegistryConfig{} + } + if services.Registry.Local.Persistence == nil { + services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{} + } - if len(services.Registry.Local.Persistence.FilePersistence.Path) == 0 { - services.Registry.Local.Persistence.FilePersistence.Path = defaultRegistryPath(cr) - } + if services.Registry.Local.Persistence.DBPersistence == nil { + if services.Registry.Local.Persistence.FilePersistence == nil { + services.Registry.Local.Persistence.FilePersistence = &feastdevv1.RegistryFilePersistence{} + } - ensurePVCDefaults(services.Registry.Local.Persistence.FilePersistence.PvcConfig, RegistryFeastType) + if len(services.Registry.Local.Persistence.FilePersistence.Path) == 0 { + services.Registry.Local.Persistence.FilePersistence.Path = defaultRegistryPath(cr) } - if services.Registry.Local.Server != nil { - setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs) - // Set default for GRPC: true if nil - if services.Registry.Local.Server.GRPC == nil { - defaultGRPC := true - services.Registry.Local.Server.GRPC = &defaultGRPC - } + ensurePVCDefaults(services.Registry.Local.Persistence.FilePersistence.PvcConfig, RegistryFeastType) + } + if services.Registry.Local.Server != nil { + setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs) + // Set default for GRPC: true if nil + if services.Registry.Local.Server.GRPC == nil { + defaultGRPC := true + services.Registry.Local.Server.GRPC = &defaultGRPC } - } else if services.Registry.Remote.FeastRef != nil && len(services.Registry.Remote.FeastRef.Namespace) == 0 { - services.Registry.Remote.FeastRef.Namespace = cr.Namespace } + return } - if services.OfflineStore != nil { - if services.OfflineStore.Persistence == nil { - services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{} - } + if services.Registry.Remote.FeastRef != nil && len(services.Registry.Remote.FeastRef.Namespace) == 0 { + services.Registry.Remote.FeastRef.Namespace = cr.Namespace + } +} - if services.OfflineStore.Persistence.DBPersistence == nil { - if services.OfflineStore.Persistence.FilePersistence == nil { - services.OfflineStore.Persistence.FilePersistence = &feastdevv1.OfflineStoreFilePersistence{} - } +func applyOfflineDefaults(services *feastdevv1.FeatureStoreServices) { + if services.OfflineStore == nil { + return + } - if len(services.OfflineStore.Persistence.FilePersistence.Type) == 0 { - services.OfflineStore.Persistence.FilePersistence.Type = string(OfflineFilePersistenceDaskConfigType) - } + if services.OfflineStore.Persistence == nil { + services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{} + } - ensurePVCDefaults(services.OfflineStore.Persistence.FilePersistence.PvcConfig, OfflineFeastType) + if services.OfflineStore.Persistence.DBPersistence == nil { + if services.OfflineStore.Persistence.FilePersistence == nil { + services.OfflineStore.Persistence.FilePersistence = &feastdevv1.OfflineStoreFilePersistence{} } - if services.OfflineStore.Server != nil { - setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs) + if len(services.OfflineStore.Persistence.FilePersistence.Type) == 0 { + services.OfflineStore.Persistence.FilePersistence.Type = string(OfflineFilePersistenceDaskConfigType) } + + ensurePVCDefaults(services.OfflineStore.Persistence.FilePersistence.PvcConfig, OfflineFeastType) } + if services.OfflineStore.Server != nil { + setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs) + } +} + +func applyOnlineDefaults(cr *feastdevv1.FeatureStore, services *feastdevv1.FeatureStoreServices) { // default to onlineStore service deployment if services.OnlineStore == nil { services.OnlineStore = &feastdevv1.OnlineStore{} @@ -184,32 +209,40 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) } - if services.OnlineStore.Server == nil && services.OnlineStore.Grpc == nil { - services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + applyOnlineServerDefaults(services.OnlineStore) +} + +func applyOnlineServerDefaults(onlineStore *feastdevv1.OnlineStore) { + if onlineStore.Server == nil && onlineStore.Grpc == nil { + onlineStore.Server = &feastdevv1.ServerConfigs{} } - if services.OnlineStore.Server != nil { - setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) + if onlineStore.Server != nil { + setDefaultCtrConfigs(&onlineStore.Server.ContainerConfigs.DefaultCtrConfigs) } - if services.OnlineStore.Grpc != nil { - setDefaultCtrConfigs(&services.OnlineStore.Grpc.ContainerConfigs.DefaultCtrConfigs) - if services.OnlineStore.Grpc.Port == nil { + if onlineStore.Grpc != nil { + setDefaultCtrConfigs(&onlineStore.Grpc.ContainerConfigs.DefaultCtrConfigs) + if onlineStore.Grpc.Port == nil { defaultPort := DefaultOnlineGrpcPort - services.OnlineStore.Grpc.Port = &defaultPort + onlineStore.Grpc.Port = &defaultPort } - if services.OnlineStore.Grpc.MaxWorkers == nil { + if onlineStore.Grpc.MaxWorkers == nil { defaultMaxWorkers := int32(10) - services.OnlineStore.Grpc.MaxWorkers = &defaultMaxWorkers + onlineStore.Grpc.MaxWorkers = &defaultMaxWorkers } - if services.OnlineStore.Grpc.RegistryTTLSeconds == nil { + if onlineStore.Grpc.RegistryTTLSeconds == nil { defaultRegistryTTL := int32(5) - services.OnlineStore.Grpc.RegistryTTLSeconds = &defaultRegistryTTL + onlineStore.Grpc.RegistryTTLSeconds = &defaultRegistryTTL } } +} +func applyUiDefaults(services *feastdevv1.FeatureStoreServices) { if services.UI != nil { setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs) } +} +func applyCronJobDefaults(applied *feastdevv1.FeatureStoreSpec) { if applied.CronJob == nil { applied.CronJob = &feastdevv1.FeastCronJob{} } From f396a6d23dc8098009386cea482ac92d4bf24429 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 15:01:40 +0100 Subject: [PATCH 05/24] feat: Add per-service scaling and gRPC support Signed-off-by: Jatin Kumar --- .../api/v1/featurestore_types.go | 34 ++-- .../featurestore_controller_db_store_test.go | 52 +++-- .../featurestore_controller_ephemeral_test.go | 102 +++++++--- ...restore_controller_kubernetes_auth_test.go | 47 +++-- .../featurestore_controller_loglevel_test.go | 94 ++++++--- ...eaturestore_controller_objectstore_test.go | 102 ++++------ .../featurestore_controller_oidc_auth_test.go | 61 +++--- .../featurestore_controller_pvc_test.go | 142 +++++++------- .../featurestore_controller_test.go | 181 +++++++----------- .../featurestore_controller_tls_test.go | 143 +++++++------- ...tore_controller_volume_volumemount_test.go | 54 ++++-- .../controller/services/services_test.go | 35 ++-- .../controller/services/services_types.go | 30 +-- 13 files changed, 562 insertions(+), 515 deletions(-) diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index 18505aa03d2..0a6fd26f834 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -30,15 +30,15 @@ const ( FailedPhase = "Failed" // Feast condition types: - ClientReadyType = "Client" - OfflineStoreReadyType = "OfflineStore" - OnlineStoreReadyType = "OnlineStore" + ClientReadyType = "Client" + OfflineStoreReadyType = "OfflineStore" + OnlineStoreReadyType = "OnlineStore" OnlineStoreGrpcReadyType = "OnlineStoreGrpc" - RegistryReadyType = "Registry" - UIReadyType = "UI" - ReadyType = "FeatureStore" - AuthorizationReadyType = "Authorization" - CronJobReadyType = "CronJob" + RegistryReadyType = "Registry" + UIReadyType = "UI" + ReadyType = "FeatureStore" + AuthorizationReadyType = "Authorization" + CronJobReadyType = "CronJob" // Feast condition reasons: ReadyReason = "Ready" @@ -77,9 +77,9 @@ type FeatureStoreSpec struct { FeastProjectDir *FeastProjectDir `json:"feastProjectDir,omitempty"` Services *FeatureStoreServices `json:"services,omitempty"` // FeatureServer configures the Feast feature server, including MCP support. - FeatureServer *FeatureServerConfig `json:"feature_server,omitempty"` - AuthzConfig *AuthzConfig `json:"authz,omitempty"` - CronJob *FeastCronJob `json:"cronJob,omitempty"` + FeatureServer *FeatureServerConfig `json:"feature_server,omitempty"` + AuthzConfig *AuthzConfig `json:"authz,omitempty"` + CronJob *FeastCronJob `json:"cronJob,omitempty"` } // FeatureServerConfig defines feature server configuration settings. @@ -387,7 +387,7 @@ var ValidOfflineStoreDBStorePersistenceTypes = []string{ // OnlineStore configures the online store service type OnlineStore struct { // Creates a feature server container - Server *ServerConfigs `json:"server,omitempty"` + Server *ServerConfigs `json:"server,omitempty"` // Creates a gRPC feature server container (feast listen) Grpc *GrpcServerConfigs `json:"grpc,omitempty"` Persistence *OnlineStorePersistence `json:"persistence,omitempty"` @@ -742,12 +742,12 @@ type FeatureStoreStatus struct { // ServiceHostnames defines the service hostnames in the format of :, e.g. example.svc.cluster.local:80 type ServiceHostnames struct { - OfflineStore string `json:"offlineStore,omitempty"` - OnlineStore string `json:"onlineStore,omitempty"` + OfflineStore string `json:"offlineStore,omitempty"` + OnlineStore string `json:"onlineStore,omitempty"` OnlineStoreGrpc string `json:"onlineStoreGrpc,omitempty"` - Registry string `json:"registry,omitempty"` - RegistryRest string `json:"registryRest,omitempty"` - UI string `json:"ui,omitempty"` + Registry string `json:"registry,omitempty"` + RegistryRest string `json:"registryRest,omitempty"` + UI string `json:"ui,omitempty"` } // +kubebuilder:object:root=true diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index d17bffb2377..0bcaa24482a 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -429,17 +429,18 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + for _, feastType := range []services.FeastServiceType{ + services.RegistryFeastType, + services.OfflineFeastType, + services.OnlineFeastType, + services.UIFeastType, + } { + deploy, err := getDeploymentByType(ctx, k8sClient, resource, feastType) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + } svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -528,7 +529,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -549,15 +550,9 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - registryContainer := services.GetRegistryContainer(*deploy) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.Env).To(HaveLen(1)) env := getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) @@ -601,7 +596,9 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { } Expect(repoConfig).To(Equal(testConfig)) - offlineContainer := services.GetOfflineContainer(*deploy) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) Expect(offlineContainer.Env).To(HaveLen(1)) assertEnvFrom(*offlineContainer) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) @@ -618,7 +615,9 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(err).NotTo(HaveOccurred()) Expect(repoConfigOffline).To(Equal(testConfig)) - onlineContainer := services.GetOnlineContainer(*deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.VolumeMounts).To(HaveLen(1)) Expect(onlineContainer.Env).To(HaveLen(1)) assertEnvFrom(*onlineContainer) @@ -636,7 +635,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { err = yaml.Unmarshal(envByte, repoConfigOnline) Expect(err).NotTo(HaveOccurred()) Expect(repoConfigOnline).To(Equal(testConfig)) - onlineContainer = services.GetOnlineContainer(*deploy) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(1)) // check client config @@ -694,12 +693,9 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { feast.Handler.FeatureStore = resource // check online config - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - onlineContainer = services.GetOnlineContainer(*deploy) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go index 212fa80228b..fc72a5e7d37 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -198,17 +198,30 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(offlineDeploy)).To(BeTrue()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + uiDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.UIFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + Expect(uiDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(uiDeploy)).To(BeTrue()) + Expect(uiDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -243,7 +256,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -264,16 +277,11 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.Env).To(HaveLen(1)) env := getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) @@ -306,7 +314,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { } Expect(repoConfig).To(Equal(testConfig)) - offlineContainer := services.GetOfflineContainer(*deploy) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) Expect(offlineContainer.Env).To(HaveLen(1)) assertEnvFrom(*offlineContainer) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) @@ -326,7 +337,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) Expect(repoConfigOffline).To(Equal(testConfig)) - onlineContainer := services.GetOnlineContainer(*deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(3)) Expect(onlineContainer.ImagePullPolicy).To(Equal(corev1.PullAlways)) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) @@ -395,12 +409,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { feast.Handler.FeatureStore = resource // check registry - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - registryContainer = services.GetRegistryContainer(*deploy) + registryContainer = services.GetRegistryContainer(*registryDeploy) env = getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64() @@ -417,7 +428,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(repoConfig).To(Equal(testConfig)) // check offline config - offlineContainer = services.GetRegistryContainer(*deploy) + offlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineContainer = services.GetOfflineContainer(*offlineDeploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -433,7 +446,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(repoConfigOffline).To(Equal(testConfig)) // check online config - onlineContainer = services.GetOnlineContainer(*deploy) + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -456,3 +471,32 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) }) }) + +func getDeploymentByType(ctx context.Context, c client.Client, resource *feastdevv1.FeatureStore, feastType services.FeastServiceType) (*appsv1.Deployment, error) { + deploy := &appsv1.Deployment{} + name := services.GetFeastServiceName(resource, feastType) + if err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: resource.Namespace}, deploy); err == nil { + return deploy, nil + } else if !errors.IsNotFound(err) { + return nil, err + } + + reqName, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + if err != nil { + return nil, err + } + reqType, err := labels.NewRequirement(services.ServiceTypeLabelKey, selection.Equals, []string{string(feastType)}) + if err != nil { + return nil, err + } + selector := labels.NewSelector().Add(*reqName, *reqType) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: selector} + deployList := appsv1.DeploymentList{} + if err := c.List(ctx, &deployList, listOpts); err != nil { + return nil, err + } + if len(deployList.Items) == 0 { + return nil, fmt.Errorf("deployment for %s not found", feastType) + } + return &deployList.Items[0], nil +} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go index 3bfab485e85..db10e962374 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go @@ -192,19 +192,20 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + for _, feastType := range []services.FeastServiceType{ + services.RegistryFeastType, + services.OfflineFeastType, + services.OnlineFeastType, + services.UIFeastType, + } { + deploy, err := getDeploymentByType(ctx, k8sClient, resource, feastType) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + } // check configured Roles for _, roleName := range roles { @@ -368,7 +369,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -389,15 +390,13 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { }, } - // check registry - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*deploy).Env) + env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*registryDeploy).Env) Expect(env).NotTo(BeNil()) // check registry config @@ -421,7 +420,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(repoConfig).To(Equal(&testConfig)) // check offline - offlineContainer := services.GetOfflineContainer(*deploy) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -440,7 +439,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(repoConfig).To(Equal(&testConfig)) // check online - onlineContainer := services.GetOnlineContainer(*deploy) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go b/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go index 948ddec210d..9b0412e2f21 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go @@ -165,28 +165,62 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() + // check deployments per service + registryDeploy := &appsv1.Deployment{} + registryMeta := feast.GetObjectMetaType(services.RegistryFeastType) err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + Name: registryMeta.Name, + Namespace: registryMeta.Namespace, + }, registryDeploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - command := services.GetRegistryContainer(*deploy).Command + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command := services.GetRegistryContainer(*registryDeploy).Command Expect(command).To(ContainElement("--log-level")) Expect(command).To(ContainElement("ERROR")) - command = services.GetOfflineContainer(*deploy).Command + offlineDeploy := &appsv1.Deployment{} + offlineMeta := feast.GetObjectMetaType(services.OfflineFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: offlineMeta.Name, + Namespace: offlineMeta.Namespace, + }, offlineDeploy) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(offlineDeploy)).To(BeTrue()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command = services.GetOfflineContainer(*offlineDeploy).Command Expect(command).To(ContainElement("--log-level")) Expect(command).To(ContainElement("INFO")) - command = services.GetOnlineContainer(*deploy).Command + onlineDeploy := &appsv1.Deployment{} + onlineMeta := feast.GetObjectMetaType(services.OnlineFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: onlineMeta.Name, + Namespace: onlineMeta.Namespace, + }, onlineDeploy) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command = services.GetOnlineContainer(*onlineDeploy).Command Expect(command).To(ContainElement("--log-level")) Expect(command).To(ContainElement("DEBUG")) + + uiDeploy := &appsv1.Deployment{} + uiMeta := feast.GetObjectMetaType(services.UIFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: uiMeta.Name, + Namespace: uiMeta.Namespace, + }, uiDeploy) + Expect(err).NotTo(HaveOccurred()) + Expect(uiDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(uiDeploy)).To(BeTrue()) + Expect(uiDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command = services.GetUIContainer(*uiDeploy).Command + Expect(command).To(ContainElement("--log-level")) + Expect(command).To(ContainElement("INFO")) }) It("should not include --log-level parameter when logLevel is not specified for any service", func() { @@ -227,22 +261,38 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() + // check deployments per service + registryDeploy := &appsv1.Deployment{} + registryMeta := feast.GetObjectMetaType(services.RegistryFeastType) err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + Name: registryMeta.Name, + Namespace: registryMeta.Namespace, + }, registryDeploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(3)) - command := services.GetRegistryContainer(*deploy).Command + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command := services.GetRegistryContainer(*registryDeploy).Command Expect(command).NotTo(ContainElement("--log-level")) - command = services.GetOnlineContainer(*deploy).Command + onlineDeploy := &appsv1.Deployment{} + onlineMeta := feast.GetObjectMetaType(services.OnlineFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: onlineMeta.Name, + Namespace: onlineMeta.Namespace, + }, onlineDeploy) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command = services.GetOnlineContainer(*onlineDeploy).Command Expect(command).NotTo(ContainElement("--log-level")) - command = services.GetUIContainer(*deploy).Command + uiDeploy := &appsv1.Deployment{} + uiMeta := feast.GetObjectMetaType(services.UIFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: uiMeta.Name, + Namespace: uiMeta.Namespace, + }, uiDeploy) + Expect(err).NotTo(HaveOccurred()) + Expect(uiDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + command = services.GetUIContainer(*uiDeploy).Command Expect(command).NotTo(ContainElement("--log-level")) }) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go index 37d22094147..8ddf0d7d8f5 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -175,23 +175,25 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) - Expect(services.GetRegistryContainer(*deploy)).NotTo(BeNil()) - Expect(services.GetOnlineContainer(*deploy)).NotTo(BeNil()) - Expect(services.GetOfflineContainer(*deploy)).To(BeNil()) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(services.GetRegistryContainer(*registryDeploy)).NotTo(BeNil()) + Expect(services.GetOfflineContainer(*registryDeploy)).To(BeNil()) + Expect(registryDeploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(services.GetOnlineContainer(*onlineDeploy)).NotTo(BeNil()) // update S3 additional args and reconcile resourceNew := resource.DeepCopy() @@ -217,13 +219,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.S3AdditionalKwargs).To(Equal(&newS3AdditionalKwargs)) // check registry deployment - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) - registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryDeploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.VolumeMounts).To(HaveLen(1)) }) @@ -250,7 +249,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(2)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -271,24 +270,18 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) - Expect(services.GetRegistryContainer(*deploy)).NotTo(BeNil()) - Expect(services.GetOnlineContainer(*deploy)).NotTo(BeNil()) - Expect(services.GetOfflineContainer(*deploy)).To(BeNil()) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) - env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(services.GetRegistryContainer(*registryDeploy)).NotTo(BeNil()) + Expect(services.GetOfflineContainer(*registryDeploy)).To(BeNil()) + Expect(registryDeploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(registryDeploy.Spec.Template.Spec.Containers[0].Env) Expect(env).NotTo(BeNil()) // check registry config @@ -348,12 +341,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { feast.Handler.FeatureStore = resource // check registry config - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + env = getFeatureStoreYamlEnvVar(registryDeploy.Spec.Template.Spec.Containers[0].Env) Expect(env).NotTo(BeNil()) fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64() Expect(err).NotTo(HaveOccurred()) @@ -397,24 +387,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast := services.FeastServices{ - Handler: handler.FeastHandler{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, - }, - } - - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*deploy).Env) + env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*registryDeploy).Env) Expect(env).NotTo(BeNil()) // decode feature_store.yaml and verify registry cache settings diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go index bb5cc4fb4b2..96af15360cc 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -211,25 +211,28 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) - Expect(services.GetOfflineContainer(*deploy).VolumeMounts).To(HaveLen(1)) - Expect(services.GetOnlineContainer(*deploy).VolumeMounts).To(HaveLen(1)) - Expect(services.GetRegistryContainer(*deploy).VolumeMounts).To(HaveLen(1)) - - assertEnvFrom(*services.GetOnlineContainer(*deploy)) - assertEnvFrom(*services.GetOfflineContainer(*deploy)) + for _, feastType := range []services.FeastServiceType{ + services.RegistryFeastType, + services.OfflineFeastType, + services.OnlineFeastType, + services.UIFeastType, + } { + deploy, err := getDeploymentByType(ctx, k8sClient, resource, feastType) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(1)) + } + + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + assertEnvFrom(*services.GetOnlineContainer(*onlineDeploy)) + assertEnvFrom(*services.GetOfflineContainer(*offlineDeploy)) // check Feast Role feastRole := &rbacv1.Role{} @@ -309,7 +312,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -330,15 +333,13 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*deploy).Env) + env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*registryDeploy).Env) Expect(env).NotTo(BeNil()) // check registry config @@ -371,7 +372,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(repoConfig).To(Equal(testConfig)) // check offline - env = getFeatureStoreYamlEnvVar(services.GetOfflineContainer(*deploy).Env) + env = getFeatureStoreYamlEnvVar(services.GetOfflineContainer(*offlineDeploy).Env) Expect(env).NotTo(BeNil()) // check offline config @@ -387,7 +388,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(repoConfig).To(Equal(testConfig)) // check online - env = getFeatureStoreYamlEnvVar(services.GetOnlineContainer(*deploy).Env) + env = getFeatureStoreYamlEnvVar(services.GetOnlineContainer(*onlineDeploy).Env) Expect(env).NotTo(BeNil()) // check online config diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index 8e7303cee34..f6c68df8d07 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -277,33 +277,41 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { MountPath: "/" + ephemeralName, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(3)) - Expect(deploy.Spec.Template.Spec.SecurityContext).NotTo(Equal(securityContext)) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(3)) - Expect(deploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) - name := feast.GetFeastServiceName(services.RegistryFeastType) - regVol := services.GetRegistryVolume(feast.Handler.FeatureStore, deploy.Spec.Template.Spec.Volumes) - Expect(regVol.Name).To(Equal(name)) - Expect(regVol.PersistentVolumeClaim.ClaimName).To(Equal(name)) - - offlineContainer := services.GetOfflineContainer(*deploy) - Expect(offlineContainer.VolumeMounts).To(HaveLen(3)) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.SecurityContext).NotTo(Equal(securityContext)) + Expect(registryDeploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) + + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(offlineDeploy)).To(BeTrue()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.SecurityContext).NotTo(Equal(securityContext)) + Expect(offlineDeploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) + + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.SecurityContext).NotTo(Equal(securityContext)) + Expect(onlineDeploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) + + offlinePvcName := feast.GetFeastServiceName(services.OfflineFeastType) + offlineVol := services.GetOfflineVolume(feast.Handler.FeatureStore, offlineDeploy.Spec.Template.Spec.Volumes) + Expect(offlineVol.Name).To(Equal(offlinePvcName)) + Expect(offlineVol.PersistentVolumeClaim.ClaimName).To(Equal(offlinePvcName)) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) + Expect(offlineContainer).NotTo(BeNil()) Expect(offlineContainer.VolumeMounts).NotTo(ContainElement(ephemeralVolMount)) offlineVolMount := services.GetOfflineVolumeMount(feast.Handler.FeatureStore, offlineContainer.VolumeMounts) Expect(offlineVolMount.MountPath).To(Equal(offlineStoreMountPath)) - offlinePvcName := feast.GetFeastServiceName(services.OfflineFeastType) Expect(offlineVolMount.Name).To(Equal(offlinePvcName)) - assertEnvFrom(*offlineContainer) // check offline pvc @@ -319,18 +327,16 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(pvc.Spec.Resources.Requests.Storage().String()).To(Equal(services.DefaultOfflineStorageRequest)) Expect(pvc.DeletionTimestamp).To(BeNil()) - // check online onlinePvcName := feast.GetFeastServiceName(services.OnlineFeastType) - onlineVol := services.GetOnlineVolume(feast.Handler.FeatureStore, deploy.Spec.Template.Spec.Volumes) + onlineVol := services.GetOnlineVolume(feast.Handler.FeatureStore, onlineDeploy.Spec.Template.Spec.Volumes) Expect(onlineVol.Name).To(Equal(onlinePvcName)) Expect(onlineVol.PersistentVolumeClaim.ClaimName).To(Equal(onlinePvcName)) - onlineContainer := services.GetOnlineContainer(*deploy) - Expect(onlineContainer.VolumeMounts).To(HaveLen(3)) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) + Expect(onlineContainer).NotTo(BeNil()) Expect(onlineContainer.VolumeMounts).NotTo(ContainElement(ephemeralVolMount)) onlineVolMount := services.GetOnlineVolumeMount(feast.Handler.FeatureStore, onlineContainer.VolumeMounts) Expect(onlineVolMount.MountPath).To(Equal(onlineStoreMountPath)) Expect(onlineVolMount.Name).To(Equal(onlinePvcName)) - assertEnvFrom(*onlineContainer) // check online pvc @@ -346,13 +352,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(pvc.Spec.Resources.Requests.Storage().String()).To(Equal(services.DefaultOnlineStorageRequest)) Expect(pvc.DeletionTimestamp).To(BeNil()) - // check registry registryPvcName := feast.GetFeastServiceName(services.RegistryFeastType) - registryVol := services.GetRegistryVolume(feast.Handler.FeatureStore, deploy.Spec.Template.Spec.Volumes) + registryVol := services.GetRegistryVolume(feast.Handler.FeatureStore, registryDeploy.Spec.Template.Spec.Volumes) Expect(registryVol.Name).To(Equal(registryPvcName)) Expect(registryVol.PersistentVolumeClaim.ClaimName).To(Equal(registryPvcName)) - registryContainer := services.GetRegistryContainer(*deploy) - Expect(registryContainer.VolumeMounts).To(HaveLen(3)) + registryContainer := services.GetRegistryContainer(*registryDeploy) + Expect(registryContainer).NotTo(BeNil()) Expect(registryContainer.VolumeMounts).NotTo(ContainElement(ephemeralVolMount)) registryVolMount := services.GetRegistryVolumeMount(feast.Handler.FeatureStore, registryContainer.VolumeMounts) Expect(registryVolMount.MountPath).To(Equal(registryMountPath)) @@ -389,22 +394,24 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { feast.Handler.FeatureStore = resource Expect(resource.Status.Applied.Services.OnlineStore.Persistence.FilePersistence.PvcConfig).To(BeNil()) - // check online deployment/container - deploy = &appsv1.Deployment{} - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + // check deployments after removing online PVC + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) + Expect(services.GetOnlineContainer(*onlineDeploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) + + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(3)) - Expect(deploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) - Expect(services.GetOnlineContainer(*deploy).VolumeMounts).To(HaveLen(3)) - Expect(services.GetOnlineContainer(*deploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) - Expect(services.GetRegistryContainer(*deploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) - Expect(services.GetOfflineContainer(*deploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) + Expect(registryDeploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) + Expect(services.GetRegistryContainer(*registryDeploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) + + offlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) + Expect(services.GetOfflineContainer(*offlineDeploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) // check online pvc is deleted - log.FromContext(feast.Handler.Context).Info("Checking deletion of", "PersistentVolumeClaim", deploy.Name) + log.FromContext(feast.Handler.Context).Info("Checking deletion of", "PersistentVolumeClaim", onlineDeploy.Name) pvc = &corev1.PersistentVolumeClaim{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: onlinePvcName, @@ -449,7 +456,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(3)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -470,17 +477,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }, } - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + // check deployments per service + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(3)) - Expect(deploy.Spec.Template.Spec.SecurityContext).To(Equal(securityContext)) - registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.SecurityContext).To(Equal(securityContext)) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.Env).To(HaveLen(1)) env := getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) @@ -514,7 +516,11 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { } Expect(repoConfig).To(Equal(testConfig)) - offlineContainer := services.GetOfflineContainer(*deploy) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.SecurityContext).To(Equal(securityContext)) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) Expect(offlineContainer.Env).To(HaveLen(1)) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -532,7 +538,11 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(repoConfigOffline).To(Equal(testConfig)) // check online config - onlineContainer := services.GetOnlineContainer(*deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.SecurityContext).To(Equal(securityContext)) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(3)) Expect(onlineContainer.ImagePullPolicy).To(Equal(corev1.PullAlways)) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) @@ -607,13 +617,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { feast.Handler.FeatureStore = resource // check registry config - deploy = &appsv1.Deployment{} - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - registryContainer = services.GetRegistryContainer(*deploy) + registryContainer = services.GetRegistryContainer(*registryDeploy) env = getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64() @@ -630,7 +636,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(repoConfig).To(Equal(testConfig)) // check offline config - offlineContainer = services.GetOfflineContainer(*deploy) + offlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineContainer = services.GetOfflineContainer(*offlineDeploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -646,7 +654,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(repoConfigOffline).To(Equal(testConfig)) // check online config - onlineContainer = services.GetOfflineContainer(*deploy) + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 5816531c0bd..7ca12cfd126 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -200,16 +200,11 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.ReadyPhase)) - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) + Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init")) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) @@ -258,10 +253,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(3))) Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) @@ -289,10 +281,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init -t spark")) @@ -323,14 +312,9 @@ var _ = Describe("FeatureStore Controller", func() { }, } - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) + Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) Expect(deploy.Spec.Strategy.Type).To(Equal(appsv1.RecreateDeploymentStrategyType)) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) @@ -386,11 +370,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Spec.FeastProject).To(Equal(resourceNew.Spec.FeastProject)) - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, - deploy) + deploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) // Update feast object with the refreshed resource @@ -438,12 +418,7 @@ var _ = Describe("FeatureStore Controller", func() { }, } - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) err = controllerutil.RemoveControllerReference(resource, deploy, controllerReconciler.Scheme) @@ -657,17 +632,33 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(offlineDeploy)).To(BeTrue()) + Expect(offlineDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + uiDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.UIFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(uiDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(uiDeploy)).To(BeTrue()) + Expect(uiDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(uiDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -702,7 +693,7 @@ var _ = Describe("FeatureStore Controller", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) saList := corev1.ServiceAccountList{} err = k8sClient.List(ctx, &saList, listOpts) @@ -729,16 +720,11 @@ var _ = Describe("FeatureStore Controller", func() { } // check registry config - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.Env).To(HaveLen(1)) env := getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) @@ -759,8 +745,11 @@ var _ = Describe("FeatureStore Controller", func() { Expect(repoConfig).To(Equal(&testConfig)) // check offline config - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - offlineContainer := services.GetOfflineContainer(*deploy) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) Expect(offlineContainer.Env).To(HaveLen(1)) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -779,7 +768,11 @@ var _ = Describe("FeatureStore Controller", func() { Expect(repoConfigOffline).To(Equal(&testConfig)) // check online config - onlineContainer := services.GetOnlineContainer(*deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(3)) Expect(onlineContainer.ImagePullPolicy).To(Equal(corev1.PullAlways)) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) @@ -846,19 +839,15 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Spec.FeastProject).To(Equal(resourceNew.Spec.FeastProject)) - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, - deploy) + registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) // Update feast object with the refreshed resource feast.Handler.FeatureStore = resource testConfig.Project = resourceNew.Spec.FeastProject - Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) - env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(registryDeploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(registryDeploy.Spec.Template.Spec.Containers[0].Env) Expect(env).NotTo(BeNil()) fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64() @@ -895,7 +884,7 @@ var _ = Describe("FeatureStore Controller", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -921,16 +910,11 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) // check online config - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - onlineContainer := services.GetOnlineContainer(*deploy) + Expect(onlineDeploy.Spec.Template.Spec.ServiceAccountName).To(Equal(feast.GetObjectMeta().Name)) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(3)) Expect(areEnvVarArraysEqual(onlineContainer.Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue}, {Name: services.TmpFeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}})).To(BeTrue()) Expect(onlineContainer.ImagePullPolicy).To(Equal(corev1.PullAlways)) @@ -948,13 +932,9 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) Expect(areEnvVarArraysEqual(*resource.Status.Applied.Services.OnlineStore.Server.Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue + "1"}, {Name: services.TmpFeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}})).To(BeTrue()) - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) - - onlineContainer = services.GetOnlineContainer(*deploy) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(3)) Expect(areEnvVarArraysEqual(onlineContainer.Env, []corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue + "1"}, {Name: services.TmpFeatureStoreYamlEnvVar, Value: fsYamlStr}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}}}})).To(BeTrue()) }) @@ -982,7 +962,7 @@ var _ = Describe("FeatureStore Controller", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -1001,7 +981,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(3)) err = k8sClient.List(ctx, &svcList, listOpts) Expect(err).NotTo(HaveOccurred()) @@ -1022,7 +1002,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(2)) err = k8sClient.List(ctx, &svcList, listOpts) Expect(err).NotTo(HaveOccurred()) @@ -1138,12 +1118,7 @@ var _ = Describe("FeatureStore Controller", func() { Expect(svcList.Items).To(HaveLen(1)) // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) @@ -1183,12 +1158,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - deploy = &appsv1.Deployment{} - objMeta = feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.InitContainers).To(BeEmpty()) @@ -1300,21 +1270,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - feast := services.FeastServices{ - Handler: handler.FeastHandler{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, - }, - } - - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) registryContainer := services.GetRegistryContainer(*deploy) @@ -1387,12 +1343,7 @@ var _ = Describe("FeatureStore Controller", func() { } // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + deploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) Expect(err).NotTo(HaveOccurred()) err = controllerutil.RemoveControllerReference(resource, deploy, controllerReconciler.Scheme) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go index 0af097120ce..530402cdd93 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go @@ -182,17 +182,29 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(registryDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(registryDeploy)).To(BeTrue()) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(offlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(offlineDeploy)).To(BeTrue()) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(onlineDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(onlineDeploy)).To(BeTrue()) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + uiDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.UIFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + Expect(uiDeploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(controllerutil.HasControllerReference(uiDeploy)).To(BeTrue()) + Expect(uiDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -235,7 +247,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { deployList := appsv1.DeploymentList{} err = k8sClient.List(ctx, &deployList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(deployList.Items).To(HaveLen(1)) + Expect(deployList.Items).To(HaveLen(4)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) @@ -247,16 +259,10 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmList.Items).To(HaveLen(1)) - // check deployment - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + registryDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) - registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + registryContainer := services.GetRegistryContainer(*registryDeploy) Expect(registryContainer.Env).To(HaveLen(1)) env := getFeatureStoreYamlEnvVar(registryContainer.Env) Expect(env).NotTo(BeNil()) @@ -277,7 +283,9 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(repoConfig).To(Equal(&testConfig)) // check offline config - offlineContainer := services.GetOfflineContainer(*deploy) + offlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) Expect(offlineContainer.Env).To(HaveLen(1)) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -294,7 +302,9 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(repoConfigOffline).To(Equal(&testConfig)) // check online config - onlineContainer := services.GetOnlineContainer(*deploy) + onlineDeploy, err := getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer.Env).To(HaveLen(1)) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -309,7 +319,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { err = yaml.Unmarshal(envByte, repoConfigOnline) Expect(err).NotTo(HaveOccurred()) Expect(repoConfigOnline).To(Equal(&testConfig)) - Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) // check client config cm := &corev1.ConfigMap{} @@ -393,16 +403,13 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { feast.Handler.FeatureStore = resource // check registry - deploy = &appsv1.Deployment{} - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) + _, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) + Expect(err).To(HaveOccurred()) // check offline config - offlineContainer = services.GetOfflineContainer(*deploy) + offlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + offlineContainer = services.GetOfflineContainer(*offlineDeploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -424,7 +431,9 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(repoConfigOffline).To(Equal(&testConfig)) // check online config - onlineContainer = services.GetOnlineContainer(*deploy) + onlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + onlineContainer = services.GetOnlineContainer(*onlineDeploy) env = getFeatureStoreYamlEnvVar(onlineContainer.Env) Expect(env).NotTo(BeNil()) @@ -506,29 +515,22 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - feast := services.FeastServices{ - Handler: handler.FeastHandler{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, - }, - } - - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) - - Expect(deploy.Spec.Template.Spec.Volumes).To(ContainElement(HaveField("Name", configMapName))) - for _, container := range deploy.Spec.Template.Spec.Containers { - Expect(container.VolumeMounts).To(ContainElement(SatisfyAll( - HaveField("Name", configMapName), - HaveField("MountPath", tlsPathCustomCABundle), - ))) + for _, feastType := range []services.FeastServiceType{ + services.RegistryFeastType, + services.OfflineFeastType, + services.OnlineFeastType, + services.UIFeastType, + } { + deploy, err := getDeploymentByType(ctx, k8sClient, resource, feastType) + Expect(err).NotTo(HaveOccurred()) + + Expect(deploy.Spec.Template.Spec.Volumes).To(ContainElement(HaveField("Name", configMapName))) + for _, container := range deploy.Spec.Template.Spec.Containers { + Expect(container.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("Name", configMapName), + HaveField("MountPath", tlsPathCustomCABundle), + ))) + } } }) @@ -549,26 +551,19 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - feast := services.FeastServices{ - Handler: handler.FeastHandler{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, - }, - } - - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) - Expect(err).NotTo(HaveOccurred()) + for _, feastType := range []services.FeastServiceType{ + services.RegistryFeastType, + services.OfflineFeastType, + services.OnlineFeastType, + services.UIFeastType, + } { + deploy, err := getDeploymentByType(ctx, k8sClient, resource, feastType) + Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(HaveField("Name", configMapName))) - for _, container := range deploy.Spec.Template.Spec.Containers { - Expect(container.VolumeMounts).NotTo(ContainElement(HaveField("Name", configMapName))) + Expect(deploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(HaveField("Name", configMapName))) + for _, container := range deploy.Spec.Template.Spec.Containers { + Expect(container.VolumeMounts).NotTo(ContainElement(HaveField("Name", configMapName))) + } } }) }) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go b/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go index 5751227e6ad..5012f4f27f9 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go @@ -112,17 +112,17 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" }, } - deploy := &appsv1.Deployment{} - objMeta := feast.GetObjectMeta() + onlineDeploy := &appsv1.Deployment{} + onlineMeta := feast.GetObjectMetaType(services.OnlineFeastType) err = k8sClient.Get(ctx, types.NamespacedName{ - Name: objMeta.Name, - Namespace: objMeta.Namespace, - }, deploy) + Name: onlineMeta.Name, + Namespace: onlineMeta.Namespace, + }, onlineDeploy) Expect(err).NotTo(HaveOccurred()) // Extract the PodSpec from DeploymentSpec - podSpec := deploy.Spec.Template.Spec + podSpec := onlineDeploy.Spec.Template.Spec // Validate Volumes // Validate Volumes - Check if our test volume exists among multiple @@ -131,13 +131,7 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" }, Equal("test-volume"))), "Expected volume 'test-volume' to be present") // Ensure 'online' container has the test volume mount - var onlineContainer *corev1.Container - for i, container := range podSpec.Containers { - if container.Name == "online" { - onlineContainer = &podSpec.Containers[i] - break - } - } + onlineContainer := services.GetOnlineContainer(*onlineDeploy) Expect(onlineContainer).ToNot(BeNil(), "Expected to find container 'online'") // Validate that 'online' container has the test-volume mount @@ -145,14 +139,32 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" return vm.Name }, Equal("test-volume"))), "Expected 'online' container to have volume mount 'test-volume'") - // Ensure all other containers do NOT have the test volume mount - for _, container := range podSpec.Containers { - if container.Name != "online" { - Expect(container.VolumeMounts).ToNot(ContainElement(WithTransform(func(vm corev1.VolumeMount) string { - return vm.Name - }, Equal("test-volume"))), "Unexpected volume mount 'test-volume' found in container "+container.Name) - } - } + // Ensure other service deployments do NOT have the test volume mount + offlineDeploy := &appsv1.Deployment{} + offlineMeta := feast.GetObjectMetaType(services.OfflineFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: offlineMeta.Name, + Namespace: offlineMeta.Namespace, + }, offlineDeploy) + Expect(err).NotTo(HaveOccurred()) + offlineContainer := services.GetOfflineContainer(*offlineDeploy) + Expect(offlineContainer).NotTo(BeNil()) + Expect(offlineContainer.VolumeMounts).ToNot(ContainElement(WithTransform(func(vm corev1.VolumeMount) string { + return vm.Name + }, Equal("test-volume"))), "Unexpected volume mount 'test-volume' found in offline container") + + uiDeploy := &appsv1.Deployment{} + uiMeta := feast.GetObjectMetaType(services.UIFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: uiMeta.Name, + Namespace: uiMeta.Namespace, + }, uiDeploy) + Expect(err).NotTo(HaveOccurred()) + uiContainer := services.GetUIContainer(*uiDeploy) + Expect(uiContainer).NotTo(BeNil()) + Expect(uiContainer.VolumeMounts).ToNot(ContainElement(WithTransform(func(vm corev1.VolumeMount) string { + return vm.Name + }, Equal("test-volume"))), "Unexpected volume mount 'test-volume' found in UI container") }) }) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index fb384775b76..51da8535533 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -40,8 +40,12 @@ func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.N } func applySpecToStatus(fs *feastdevv1.FeatureStore) { - fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() - fs.Status.Applied.FeastProject = fs.Spec.FeastProject + if fs.Status.Applied.Services == nil && fs.Spec.Services != nil { + fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() + } + if len(fs.Status.Applied.FeastProject) == 0 { + fs.Status.Applied.FeastProject = fs.Spec.FeastProject + } Expect(k8sClient.Status().Update(context.Background(), fs)).To(Succeed()) } @@ -63,6 +67,8 @@ var _ = Describe("Registry Service", func() { } BeforeEach(func() { + // Avoid cross-test contamination from tls tests toggling the global OpenShift flag. + isOpenShift = false ctx = context.Background() typeNamespacedName = types.NamespacedName{ Name: "testfeaturestore", @@ -166,7 +172,7 @@ var _ = Describe("Registry Service", func() { ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(1)) - Expect(ports[0].ContainerPort).To(Equal(FeastServiceConstants[RegistryFeastType].TargetHttpPort)) + Expect(ports[0].ContainerPort).To(Equal(getTargetPort(RegistryFeastType, feast.getTlsConfigs(RegistryFeastType)))) Expect(ports[0].Name).To(Equal(string(RegistryFeastType))) }) @@ -179,12 +185,12 @@ var _ = Describe("Registry Service", func() { ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(1)) - Expect(ports[0].ContainerPort).To(Equal(FeastServiceConstants[RegistryFeastType].TargetRestHttpPort)) + Expect(ports[0].ContainerPort).To(Equal(getTargetRestPort(RegistryFeastType, feast.getTlsConfigs(RegistryFeastType)))) Expect(ports[0].Name).To(Equal(string(RegistryFeastType) + "-rest")) Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deployment.Spec.Template.Spec.Containers[0].Ports).To(HaveLen(1)) - Expect(deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).To(Equal(FeastServiceConstants[RegistryFeastType].TargetRestHttpPort)) + Expect(deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).To(Equal(getTargetRestPort(RegistryFeastType, feast.getTlsConfigs(RegistryFeastType)))) Expect(deployment.Spec.Template.Spec.Containers[0].Ports[0].Name).To(Equal(string(RegistryFeastType) + "-rest")) }) @@ -198,9 +204,9 @@ var _ = Describe("Registry Service", func() { ports := deployment.Spec.Template.Spec.Containers[0].Ports Expect(ports).To(HaveLen(2)) - Expect(ports[0].ContainerPort).To(Equal(FeastServiceConstants[RegistryFeastType].TargetHttpPort)) + Expect(ports[0].ContainerPort).To(Equal(getTargetPort(RegistryFeastType, feast.getTlsConfigs(RegistryFeastType)))) Expect(ports[0].Name).To(Equal(string(RegistryFeastType))) - Expect(ports[1].ContainerPort).To(Equal(FeastServiceConstants[RegistryFeastType].TargetRestHttpPort)) + Expect(ports[1].ContainerPort).To(Equal(getTargetRestPort(RegistryFeastType, feast.getTlsConfigs(RegistryFeastType)))) Expect(ports[1].Name).To(Equal(string(RegistryFeastType) + "-rest")) }) }) @@ -269,8 +275,8 @@ var _ = Describe("Registry Service", func() { // Verify NodeSelector uses online service selector only expectedNodeSelector := map[string]string{ - "node-type": "online", - "zone": "us-west-1a", + "node-type": "online", + "zone": "us-west-1a", } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -376,7 +382,14 @@ var _ = Describe("Registry Service", func() { It("should enable metrics on the online service when configured", func() { featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ - Server: &feastdevv1.ServerConfigs{Metrics: ptr(true)}, + Server: &feastdevv1.ServerConfigs{ + Metrics: ptr(true), + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, } Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) @@ -392,7 +405,7 @@ var _ = Describe("Registry Service", func() { onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) - Expect(onlineContainer.Command).To(Equal([]string{"feast", "serve", "--metrics", "-h", "0.0.0.0", "-p", "6566"})) + Expect(onlineContainer.Command).To(Equal(feast.getContainerCommand(OnlineFeastType))) Expect(onlineContainer.Ports).To(ContainElement(corev1.ContainerPort{ Name: "metrics", ContainerPort: MetricsPort, diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index b3a3c247d51..eab539b0630 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -57,15 +57,15 @@ const ( MetricsPort int32 = 8000 DefaultOnlineGrpcPort int32 = 50051 - AuthzFeastType FeastServiceType = "authorization" - OfflineFeastType FeastServiceType = "offline" - OnlineFeastType FeastServiceType = "online" + AuthzFeastType FeastServiceType = "authorization" + OfflineFeastType FeastServiceType = "offline" + OnlineFeastType FeastServiceType = "online" OnlineGrpcFeastType FeastServiceType = "online-grpc" - RegistryFeastType FeastServiceType = "registry" - UIFeastType FeastServiceType = "ui" - ClientFeastType FeastServiceType = "client" - ClientCaFeastType FeastServiceType = "client-ca" - CronJobFeastType FeastServiceType = "cronjob" + RegistryFeastType FeastServiceType = "registry" + UIFeastType FeastServiceType = "ui" + ClientFeastType FeastServiceType = "client" + ClientCaFeastType FeastServiceType = "client-ca" + CronJobFeastType FeastServiceType = "cronjob" OfflineRemoteConfigType OfflineConfigType = "remote" OfflineFilePersistenceDaskConfigType OfflineConfigType = "dask" @@ -270,14 +270,14 @@ type FeastServices struct { // RepoConfig is the Repo config. Typically loaded from feature_store.yaml. // https://rtd.feast.dev/en/stable/#feast.repo_config.RepoConfig type RepoConfig struct { - Project string `yaml:"project,omitempty"` - Provider FeastProviderType `yaml:"provider,omitempty"` - OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` - OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` - Registry RegistryConfig `yaml:"registry,omitempty"` - AuthzConfig AuthzConfig `yaml:"auth,omitempty"` + Project string `yaml:"project,omitempty"` + Provider FeastProviderType `yaml:"provider,omitempty"` + OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` + OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` + Registry RegistryConfig `yaml:"registry,omitempty"` + AuthzConfig AuthzConfig `yaml:"auth,omitempty"` FeatureServer *feastdevv1.FeatureServerConfig `yaml:"feature_server,omitempty"` - EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` + EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` } // OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline store. From ee50edfc4b00fa64c75b99d5af6334f3cf61bc11 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 18:20:02 +0100 Subject: [PATCH 06/24] test: Fix reconcile error expectations Signed-off-by: Jatin Kumar --- .../controller/featurestore_controller_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 7ca12cfd126..0dd1299f9f4 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -460,9 +460,10 @@ var _ = Describe("FeatureStore Controller", func() { cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(feastdevv1.OnlineStoreFailedReason)) Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) @@ -1405,10 +1406,10 @@ var _ = Describe("FeatureStore Controller", func() { cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(feastdevv1.OnlineStoreFailedReason)) Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) + Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) Expect(resource.Status.Phase).To(Equal(feastdevv1.FailedPhase)) }) From 891450888f6c44f1e837b4d779508ed1bf09208f Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 18:35:56 +0100 Subject: [PATCH 07/24] chore: Update install.yaml Signed-off-by: Jatin Kumar --- infra/feast-operator/dist/install.yaml | 3024 +++++++++++++++--------- 1 file changed, 1863 insertions(+), 1161 deletions(-) diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b34092d8b42..3502a923437 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -699,6 +699,56 @@ spec: x-kubernetes-validations: - message: One selection required between init or git. rule: '[has(self.git), has(self.init)].exists_one(c, c)' + feature_server: + description: FeatureServer configures the Feast feature server, including + MCP support. + properties: + enabled: + description: Whether the feature server should be launched. + type: boolean + feature_logging: + description: Feature logging configuration. + properties: + emit_timeout_micro_secs: + description: Timeout for adding new log item to the queue. + format: int32 + type: integer + enabled: + type: boolean + flush_interval_secs: + description: Interval of flushing logs to the destination + in offline store. + format: int32 + type: integer + queue_capacity: + description: Log queue capacity. + format: int32 + type: integer + write_to_disk_interval_secs: + description: Interval of dumping logs collected in memory + to local disk. + format: int32 + type: integer + type: object + mcp_enabled: + description: Enable MCP server support - defaults to false. + type: boolean + mcp_server_name: + description: MCP server name for identification. + type: string + mcp_server_version: + description: MCP server version. + type: string + mcp_transport: + description: Optional MCP transport configuration. + type: string + transformation_service_endpoint: + description: The endpoint definition for transformation_service. + type: string + type: + description: Feature server type selector (e.g. local, mcp) + type: string + type: object services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -1057,6 +1107,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -1236,158 +1291,9 @@ spec: onlineStore: description: OnlineStore configures the online store service properties: - persistence: - description: OnlineStorePersistence configures the persistence - settings for the online store service - properties: - file: - description: OnlineStoreFilePersistence configures the - file-based persistence for the online store service - properties: - path: - type: string - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object - type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath - type: object - x-kubernetes-validations: - - message: One selection is required between ref and - create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and must - not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: object - x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') - : 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: Online store does not support S3 or GS buckets. - rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' - store: - description: OnlineStoreDBStorePersistence configures - the DB store persistence for the online store service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the secret - key. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you want - to use. - enum: - - snowflake.online - - redis - - ikv - - datastore - - dynamodb - - bigtable - - postgres - - cassandra - - mysql - - hazelcast - - singlestore - - hbase - - elasticsearch - - qdrant - - couchbase.online - - milvus - - hybrid - type: string - required: - - secretRef - - type - type: object - type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, c)' - server: - description: Creates a feature server container + grpc: + description: Creates a gRPC feature server container (feast + listen) properties: env: items: @@ -1548,25 +1454,30 @@ spec: description: PullPolicy describes a policy for if/when to pull a container image type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean + maxWorkers: + description: MaxWorkers sets the maximum number of threads + for handling gRPC calls. + format: int32 + type: integer nodeSelector: additionalProperties: type: string type: object + port: + description: Port sets the gRPC server port. Defaults + to 50051 if unset. + format: int32 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds sets how often the registry + is refreshed. + format: int32 + type: integer + replicas: + description: Replicas sets the number of replicas for + the gRPC service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -1613,47 +1524,9 @@ spec: of compute resources required. type: object type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, where - TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key - names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' volumeMounts: description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + that should be mounted into the gRPC container. items: description: VolumeMount describes a mounting of a Volume within a container. @@ -1695,199 +1568,72 @@ spec: - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. + type: object + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * - CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object - type: object - type: object - registry: - description: Registry configures the registry service. One selection - is required. Local is the default setting. - properties: - local: - description: LocalRegistryConfig configures the registry service - properties: - persistence: - description: RegistryPersistence configures the persistence - settings for the registry service - properties: - file: - description: RegistryFilePersistence configures the - file-based persistence for the registry service + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. properties: - cache_mode: - description: |- - CacheMode defines the registry cache update strategy. - Allowed values are "sync" and "thread". - enum: - - none - - sync - - thread - type: string - cache_ttl_seconds: - description: CacheTTLSeconds defines the TTL (in - seconds) for the registry cache. - format: int32 - minimum: 0 - type: integer - path: - type: string - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. + create: + description: Settings for creating a new PVC properties: - create: - description: Settings for creating a new PVC + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath type: object x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - 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: Registry files 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' - store: - description: RegistryDBStorePersistence configures - the DB store persistence for the registry service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + ref: + description: Reference to an existing field properties: name: default: "" @@ -1898,146 +1644,109 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - sql - - snowflake.registry - type: string required: - - secretRef - - type + - mountPath type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') type: object x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a registry server container + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : 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: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service properties: - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: - supports metadata.name, metadata.namespace, - `metadata.labels['''']`, `metadata.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source - of a set of ConfigMaps + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - configMapRef: - description: The ConfigMap to select from + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: + key: + description: The key to select. + type: string name: default: "" description: |- @@ -2047,17 +1756,62 @@ spec: type: string optional: description: Specify whether the ConfigMap - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string name: default: "" description: |- @@ -2067,256 +1821,159 @@ spec: type: string optional: description: Specify whether the Secret - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic type: object - type: array - grpc: - description: Enable gRPC registry server. Defaults - to true if unset. - type: boolean - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - restAPI: - description: Enable REST API registry server. - type: boolean - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of - a Volume within a container. + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string name: - description: This must match the Name of a Volume. - type: string - readOnly: + default: "" description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined type: boolean - recursiveReadOnly: + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - subPath: + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume - from which the container's volume should be - mounted. + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string required: - - mountPath - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object type: object - x-kubernetes-validations: - - message: At least one of restAPI or grpc must be true - rule: self.restAPI == true || self.grpc == true || !has(self.grpc) - type: object - remote: - description: RemoteRegistryConfig points to a remote feast - registry server. - properties: - feastRef: - description: Reference to an existing `FeatureStore` CR - in the same k8s cluster. - properties: - name: - description: Name of the FeatureStore - type: string - namespace: - description: Namespace of the FeatureStore - type: string - required: - - name - type: object - hostname: - description: Host address of the remote registry service - - :, e.g. `registry..svc.cluster.local:80` - type: string tls: - description: TlsRemoteRegistryConfigs configures client - TLS for a remote feast registry. + description: TlsConfigs configures server TLS for a feast + service. properties: - certName: - description: defines the configmap key name for the - client TLS cert. - type: string - configMapRef: - description: references the local k8s configmap where - the TLS cert resides + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -2327,68 +1984,754 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - certName - - configMapRef + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer type: object type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, c)' - securityContext: - description: PodSecurityContext holds pod-level security attributes - and common container settings. + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. properties: - appArmorProfile: - description: appArmorProfile is the AppArmor options to use - by the containers in this pod. - properties: - localhostProfile: - description: localhostProfile indicates a profile loaded - on the node that should be used. - type: string - type: - description: type indicates which kind of AppArmor profile - will be applied. - type: string - required: - - type - type: object - fsGroup: - description: A special supplemental group that applies to - all containers in a pod. - format: int64 - type: integer - fsGroupChangePolicy: - description: |- - fsGroupChangePolicy defines behavior of changing ownership and permission of the volume - before being exposed inside Pod. - type: string - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. + local: + description: LocalRegistryConfig configures the registry service properties: - level: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - 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: Registry files 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' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: description: Level is SELinux level label that applies to the container. type: string @@ -2651,6 +2994,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for the + service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -4832,229 +5180,572 @@ spec: description: The ConfigMap to select from properties: name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - ikv + - couchbase + - clickhouse + type: string + type: object + type: object + x-kubernetes-validations: + - message: One selection required between init or git. + rule: '[has(self.git), has(self.init)].exists_one(c, c)' + feature_server: + description: FeatureServer configures the Feast feature server, + including MCP support. + properties: + enabled: + description: Whether the feature server should be launched. + type: boolean + feature_logging: + description: Feature logging configuration. + properties: + emit_timeout_micro_secs: + description: Timeout for adding new log item to the queue. + format: int32 + type: integer + enabled: + type: boolean + flush_interval_secs: + description: Interval of flushing logs to the destination + in offline store. + format: int32 + type: integer + queue_capacity: + description: Log queue capacity. + format: int32 + type: integer + write_to_disk_interval_secs: + description: Interval of dumping logs collected in memory + to local disk. + format: int32 + type: integer + type: object + mcp_enabled: + description: Enable MCP server support - defaults to false. + type: boolean + mcp_server_name: + description: MCP server name for identification. + type: string + mcp_server_version: + description: MCP server version. + type: string + mcp_transport: + description: Optional MCP transport configuration. + type: string + transformation_service_endpoint: + description: The endpoint definition for transformation_service. + type: string + type: + description: Feature server type selector (e.g. local, mcp) + type: string + type: object + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be + unavailable during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service + properties: + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" + value: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - featureRepoPath: - description: FeatureRepoPath is the relative path to the - feature repo subdirectory. Default is 'feature_repo'. - type: string - ref: - description: Reference to a branch / tag / commit - type: string - url: - description: The repository URL to clone from. - type: string - required: - - url - type: object - x-kubernetes-validations: - - message: RepoPath must be a file name only, with no slashes. - rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') - : true' - init: - description: FeastInitOptions defines how to run a `feast - init`. - properties: - minimal: - type: boolean - template: - description: Template for the created project - enum: - - local - - gcp - - aws - - snowflake - - spark - - postgres - - hbase - - cassandra - - hazelcast - - ikv - - couchbase - - clickhouse - type: string - type: object - type: object - x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. - properties: - deploymentStrategy: - description: DeploymentStrategy describes how to replace existing - pods with new ones. - properties: - rollingUpdate: - description: |- - Rolling update config params. Present only if DeploymentStrategyType = - RollingUpdate. - properties: - maxSurge: - anyOf: - - type: integer - - type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: description: |- - The maximum number of pods that can be scheduled above the desired number of - pods. - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: The maximum number of pods that can be - unavailable during the update. - x-kubernetes-int-or-string: true - type: object - type: - description: Type of deployment. Can be "Recreate" or - "RollingUpdate". Default is RollingUpdate. - type: string - type: object - disableInitContainers: - description: Disable the 'feast repo initialization' initContainer - type: boolean - offlineStore: - description: OfflineStore configures the offline store service - properties: - persistence: - description: OfflineStorePersistence configures the persistence - settings for the offline store service - properties: - file: - description: OfflineStoreFilePersistence configures - the file-based persistence for the offline store - service + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute + resource requirements. properties: - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object - type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object - x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and - must not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: - enum: - - file - - dask - - duckdb - type: string type: object - store: - description: OfflineStoreDBStorePersistence configures - the DB store persistence for the offline store service + tls: + description: TlsConfigs configures server TLS for + a feast service. properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -5065,33 +5756,110 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - snowflake.offline - - bigquery - - redshift - - spark - - postgres - - trino - - athena - - mssql - - couchbase.offline - - clickhouse - - ray - type: string - required: - - secretRef - - type + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer type: object type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a remote offline server container + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + grpc: + description: Creates a gRPC feature server container (feast + listen) properties: env: items: @@ -5255,25 +6023,30 @@ spec: description: PullPolicy describes a policy for if/when to pull a container image type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean + maxWorkers: + description: MaxWorkers sets the maximum number of + threads for handling gRPC calls. + format: int32 + type: integer nodeSelector: additionalProperties: type: string type: object + port: + description: Port sets the gRPC server port. Defaults + to 50051 if unset. + format: int32 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds sets how often the + registry is refreshed. + format: int32 + type: integer + replicas: + description: Replicas sets the number of replicas + for the gRPC service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -5320,47 +6093,9 @@ spec: of compute resources required. type: object type: object - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' volumeMounts: description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + that should be mounted into the gRPC container. items: description: VolumeMount describes a mounting of a Volume within a container. @@ -5403,57 +6138,7 @@ spec: - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object type: object - type: object - onlineStore: - description: OnlineStore configures the online store service - properties: persistence: description: OnlineStorePersistence configures the persistence settings for the online store service @@ -5792,6 +6477,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -6339,6 +7029,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas + for the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -6898,6 +7593,11 @@ spec: additionalProperties: type: string type: object + replicas: + description: Replicas sets the number of replicas for + the service deployment. + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -8577,6 +9277,8 @@ spec: type: string onlineStore: type: string + onlineStoreGrpc: + type: string registry: type: string registryRest: @@ -17508,9 +18210,9 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.59.0 + value: null - name: RELATED_IMAGE_CRON_JOB - value: quay.io/openshift/origin-cli:4.17 + value: null image: quay.io/feastdev/feast-operator:0.59.0 livenessProbe: httpGet: From 9d81a143739ab3159c915062ab8a8b3124de6b4f Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 18:43:43 +0100 Subject: [PATCH 08/24] chore: Update generated manifests and docs Signed-off-by: Jatin Kumar --- infra/feast-operator/dist/install.yaml | 4 +- infra/feast-operator/docs/api/markdown/ref.md | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 3502a923437..11db782529d 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -18210,9 +18210,9 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: null + value: quay.io/feastdev/feature-server:0.59.0 - name: RELATED_IMAGE_CRON_JOB - value: null + value: quay.io/openshift/origin-cli:4.17 image: quay.io/feastdev/feast-operator:0.59.0 livenessProbe: httpGet: diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 64998542d2f..39a7358b1b6 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -36,6 +36,7 @@ ContainerConfigs k8s container settings for the server _Appears in:_ - [CronJobContainerConfigs](#cronjobcontainerconfigs) +- [GrpcServerConfigs](#grpcserverconfigs) - [RegistryServerConfigs](#registryserverconfigs) - [ServerConfigs](#serverconfigs) @@ -79,6 +80,7 @@ DefaultCtrConfigs k8s container settings that are applied by default _Appears in:_ - [ContainerConfigs](#containerconfigs) - [CronJobContainerConfigs](#cronjobcontainerconfigs) +- [GrpcServerConfigs](#grpcserverconfigs) - [RegistryServerConfigs](#registryserverconfigs) - [ServerConfigs](#serverconfigs) @@ -156,6 +158,46 @@ _Appears in:_ | `init` _[FeastInitOptions](#feastinitoptions)_ | | +#### FeatureLoggingConfig + + + +FeatureLoggingConfig defines feature server logging settings. + +_Appears in:_ +- [FeatureServerConfig](#featureserverconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | | +| `flush_interval_secs` _integer_ | Interval of flushing logs to the destination in offline store. | +| `write_to_disk_interval_secs` _integer_ | Interval of dumping logs collected in memory to local disk. | +| `queue_capacity` _integer_ | Log queue capacity. | +| `emit_timeout_micro_secs` _integer_ | Timeout for adding new log item to the queue. | + + +#### FeatureServerConfig + + + +FeatureServerConfig defines feature server configuration settings. +Fields are aligned with Feast's feature_store.yaml feature_server schema. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `type` _string_ | Feature server type selector (e.g. local, mcp) | +| `enabled` _boolean_ | Whether the feature server should be launched. | +| `mcp_enabled` _boolean_ | Enable MCP server support - defaults to false. | +| `mcp_server_name` _string_ | MCP server name for identification. | +| `mcp_server_version` _string_ | MCP server version. | +| `mcp_transport` _string_ | Optional MCP transport configuration. | +| `transformation_service_endpoint` _string_ | The endpoint definition for transformation_service. | +| `feature_logging` _[FeatureLoggingConfig](#featureloggingconfig)_ | Feature logging configuration. | + + #### FeatureStore @@ -224,6 +266,7 @@ _Appears in:_ | `feastProject` _string_ | FeastProject is the Feast project id. This can be any alphanumeric string with underscores and hyphens, but it cannot start with an underscore or hyphen. Required. | | `feastProjectDir` _[FeastProjectDir](#feastprojectdir)_ | | | `services` _[FeatureStoreServices](#featurestoreservices)_ | | +| `feature_server` _[FeatureServerConfig](#featureserverconfig)_ | FeatureServer configures the Feast feature server, including MCP support. | | `authz` _[AuthzConfig](#authzconfig)_ | | | `cronJob` _[FeastCronJob](#feastcronjob)_ | | @@ -269,6 +312,30 @@ OR 'url."https://api:\${TOKEN}@github.com/".insteadOf': 'https://github.com/' | | `envFrom` _[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core)_ | | +#### GrpcServerConfigs + + + +GrpcServerConfigs creates a gRPC feature server for the online store. + +_Appears in:_ +- [OnlineStore](#onlinestore) + +| Field | Description | +| --- | --- | +| `image` _string_ | | +| `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core)_ | | +| `envFrom` _[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core)_ | | +| `imagePullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | +| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | +| `nodeSelector` _map[string]string_ | | +| `replicas` _integer_ | Replicas sets the number of replicas for the gRPC service deployment. | +| `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the gRPC container. | +| `port` _integer_ | Port sets the gRPC server port. Defaults to 50051 if unset. | +| `maxWorkers` _integer_ | MaxWorkers sets the maximum number of threads for handling gRPC calls. | +| `registryTTLSeconds` _integer_ | RegistryTTLSeconds sets how often the registry is refreshed. | + + #### JobSpec @@ -505,6 +572,7 @@ _Appears in:_ | Field | Description | | --- | --- | | `server` _[ServerConfigs](#serverconfigs)_ | Creates a feature server container | +| `grpc` _[GrpcServerConfigs](#grpcserverconfigs)_ | Creates a gRPC feature server container (feast listen) | | `persistence` _[OnlineStorePersistence](#onlinestorepersistence)_ | | @@ -563,6 +631,7 @@ OptionalCtrConfigs k8s container settings that are optional _Appears in:_ - [ContainerConfigs](#containerconfigs) - [CronJobContainerConfigs](#cronjobcontainerconfigs) +- [GrpcServerConfigs](#grpcserverconfigs) - [RegistryServerConfigs](#registryserverconfigs) - [ServerConfigs](#serverconfigs) @@ -700,6 +769,7 @@ _Appears in:_ | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | | `nodeSelector` _map[string]string_ | | | `tls` _[TlsConfigs](#tlsconfigs)_ | | +| `replicas` _integer_ | Replicas sets the number of replicas for the service deployment. | | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | @@ -766,6 +836,7 @@ _Appears in:_ | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | | `nodeSelector` _map[string]string_ | | | `tls` _[TlsConfigs](#tlsconfigs)_ | | +| `replicas` _integer_ | Replicas sets the number of replicas for the service deployment. | | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | @@ -790,6 +861,7 @@ _Appears in:_ | --- | --- | | `offlineStore` _string_ | | | `onlineStore` _string_ | | +| `onlineStoreGrpc` _string_ | | | `registry` _string_ | | | `registryRest` _string_ | | | `ui` _string_ | | From 836ca0967826d3fa4a27a235ef1ac41724e0efec Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 2 Feb 2026 22:48:00 +0100 Subject: [PATCH 09/24] chore: Addressed devin ai comments Signed-off-by: Jatin Kumar --- .../api/v1/featurestore_types.go | 2 + .../api/v1/zz_generated.deepcopy.go | 5 + .../crd/bases/feast.dev_featurestores.yaml | 74 ++++++++++++ infra/feast-operator/dist/install.yaml | 74 ++++++++++++ infra/feast-operator/docs/api/markdown/ref.md | 2 + .../internal/controller/services/services.go | 28 ++++- .../controller/services/services_test.go | 107 ++++++++++++++++++ .../internal/controller/services/tls.go | 35 +++++- 8 files changed, 325 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index 0a6fd26f834..8c998686dcf 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -611,6 +611,8 @@ type GrpcServerConfigs struct { ContainerConfigs `json:",inline"` // Replicas sets the number of replicas for the gRPC service deployment. Replicas *int32 `json:"replicas,omitempty"` + // TLS configures TLS for the gRPC server. + TLS *TlsConfigs `json:"tls,omitempty"` // VolumeMounts defines the list of volumes that should be mounted into the gRPC container. VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` // Port sets the gRPC server port. Defaults to 50051 if unset. diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index 5abd433c033..cb38154a224 100644 --- a/infra/feast-operator/api/v1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -546,6 +546,11 @@ func (in *GrpcServerConfigs) DeepCopyInto(out *GrpcServerConfigs) { *out = new(int32) **out = **in } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsConfigs) + (*in).DeepCopyInto(*out) + } if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]corev1.VolumeMount, len(*in)) 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 add32ae00e4..dee504270f8 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -1516,6 +1516,43 @@ spec: of compute resources required. type: object type: object + tls: + description: TLS configures TLS for the gRPC server. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' volumeMounts: description: VolumeMounts defines the list of volumes that should be mounted into the gRPC container. @@ -6085,6 +6122,43 @@ spec: of compute resources required. type: object type: object + tls: + description: TLS configures TLS for the gRPC server. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' volumeMounts: description: VolumeMounts defines the list of volumes that should be mounted into the gRPC container. diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 11db782529d..5157ad4279b 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -1524,6 +1524,43 @@ spec: of compute resources required. type: object type: object + tls: + description: TLS configures TLS for the gRPC server. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' volumeMounts: description: VolumeMounts defines the list of volumes that should be mounted into the gRPC container. @@ -6093,6 +6130,43 @@ spec: of compute resources required. type: object type: object + tls: + description: TLS configures TLS for the gRPC server. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' volumeMounts: description: VolumeMounts defines the list of volumes that should be mounted into the gRPC container. diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 39a7358b1b6..81bc9b08bed 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -330,6 +330,7 @@ _Appears in:_ | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | | `nodeSelector` _map[string]string_ | | | `replicas` _integer_ | Replicas sets the number of replicas for the gRPC service deployment. | +| `tls` _[TlsConfigs](#tlsconfigs)_ | TLS configures TLS for the gRPC server. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the gRPC container. | | `port` _integer_ | Port sets the gRPC server port. Defaults to 50051 if unset. | | `maxWorkers` _integer_ | MaxWorkers sets the maximum number of threads for handling gRPC calls. | @@ -874,6 +875,7 @@ _Appears in:_ TlsConfigs configures server TLS for a feast service. in an openshift cluster, this is configured by default using service serving certificates. _Appears in:_ +- [GrpcServerConfigs](#grpcserverconfigs) - [RegistryServerConfigs](#registryserverconfigs) - [ServerConfigs](#serverconfigs) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 9a952d93ddf..52555e9c47f 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -291,6 +291,7 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) } } else { _ = feast.Handler.DeleteOwnedFeastObj(feast.initFeastSvc(feastType)) + _ = feast.Handler.DeleteOwnedFeastObj(feast.initFeastDeploy(feastType)) // Delete REST API service if it exists _ = feast.Handler.DeleteOwnedFeastObj(&corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -298,6 +299,8 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) Namespace: feast.Handler.FeatureStore.Namespace, }, }) + apimeta.RemoveStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) + return nil } return feast.setFeastServiceCondition(nil, feastType) } @@ -309,6 +312,16 @@ func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastDeploy(feastType)); err != nil { return err } + if feastType == RegistryFeastType { + if err := feast.Handler.DeleteOwnedFeastObj(&corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.GetFeastRestServiceName(feastType), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + }); err != nil { + return err + } + } if err := feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { return err } @@ -474,7 +487,7 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy Protocol: corev1.ProtocolTCP, }, } - probeHandler := feast.getProbeHandler(feastType, &feastdevv1.TlsConfigs{}) + probeHandler := feast.getProbeHandler(feastType, feast.getTlsConfigs(feastType)) container.StartupProbe = &corev1.Probe{ ProbeHandler: probeHandler, PeriodSeconds: 3, @@ -704,6 +717,10 @@ func (feast *FeastServices) getGrpcContainerCommand() []string { cmd = append(cmd, "--registry_ttl_sec", strconv.Itoa(int(*grpcCfg.RegistryTTLSeconds))) } } + if tls := feast.getTlsConfigs(OnlineGrpcFeastType); tls.IsTLS() { + feastTlsPath := GetTlsPath(OnlineGrpcFeastType) + cmd = append(cmd, "--key", feastTlsPath+tls.SecretKeyNames.TlsKey, "--cert", feastTlsPath+tls.SecretKeyNames.TlsCrt) + } return cmd } @@ -780,6 +797,12 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi svc.Labels = feast.getFeastTypeLabels(feastType) if feastType == OnlineGrpcFeastType { grpcPort := feast.getOnlineGrpcPort() + if feast.isOpenShiftTls(feastType) { + if len(svc.Annotations) == 0 { + svc.Annotations = map[string]string{} + } + svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = svc.Name + tlsNameSuffix + } svc.Spec = corev1.ServiceSpec{ Selector: feast.getFeastTypeLabels(feastType), Type: corev1.ServiceTypeClusterIP, @@ -1420,6 +1443,9 @@ func getTargetRestPort(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) i } func (feast *FeastServices) getProbeHandler(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) corev1.ProbeHandler { + if tls == nil { + tls = &feastdevv1.TlsConfigs{} + } targetPort := getTargetPort(feastType, tls) if feastType == OnlineGrpcFeastType { diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 51da8535533..6487ceffb7f 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -24,6 +24,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -449,6 +451,111 @@ var _ = Describe("Registry Service", func() { }) }) + Describe("Online Store Server Removal", func() { + It("should delete online deployment when server config is removed", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + // create online deployment + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + Expect(apimeta.IsStatusConditionTrue(featureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + + onlineDeploy := feast.initFeastDeploy(OnlineFeastType) + Expect(onlineDeploy).NotTo(BeNil()) + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: onlineDeploy.Name, + Namespace: onlineDeploy.Namespace, + }, onlineDeploy)).To(Succeed()) + + // remove server config (simulate grpc-only) + featureStore.Spec.Services.OnlineStore.Server = nil + featureStore.Spec.Services.OnlineStore.Grpc = &feastdevv1.GrpcServerConfigs{} + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + Expect(apimeta.FindStatusCondition(featureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeNil()) + + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: onlineDeploy.Name, + Namespace: onlineDeploy.Namespace, + }, onlineDeploy) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + + Describe("Online gRPC TLS", func() { + It("should mount TLS secret and configure command flags", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Grpc: &feastdevv1.GrpcServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: "grpc-tls", + }, + }, + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy(OnlineGrpcFeastType) + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment, OnlineGrpcFeastType)).To(Succeed()) + + _, container := getContainerByType(OnlineGrpcFeastType, deployment.Spec.Template.Spec) + Expect(container).NotTo(BeNil()) + Expect(container.Command).To(ContainElements( + "--key", "/tls/online-grpc/tls.key", + "--cert", "/tls/online-grpc/tls.crt", + )) + + hasMount := false + for _, mount := range container.VolumeMounts { + if mount.Name == "online-grpc-tls" && + mount.MountPath == "/tls/online-grpc/" && + mount.ReadOnly { + hasMount = true + break + } + } + Expect(hasMount).To(BeTrue()) + + hasVolume := false + for _, volume := range deployment.Spec.Template.Spec.Volumes { + if volume.Name == "online-grpc-tls" && + volume.Secret != nil && + volume.Secret.SecretName == "grpc-tls" { + hasVolume = true + break + } + } + Expect(hasVolume).To(BeTrue()) + }) + }) + Describe("WorkerConfigs Configuration", func() { It("should apply WorkerConfigs to the online store command", func() { // Set WorkerConfigs for online store diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go index 4a50697c5a1..8286afe52d5 100644 --- a/infra/feast-operator/internal/controller/services/tls.go +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -38,6 +38,9 @@ func (feast *FeastServices) setTlsDefaults() error { if feast.isOnlineServer() { tlsDefaults(appliedServices.OnlineStore.Server.TLS) } + if feast.isOnlineGrpcServer() { + tlsDefaults(appliedServices.OnlineStore.Grpc.TLS) + } if feast.isRegistryServer() { tlsDefaults(appliedServices.Registry.Local.Server.TLS) } @@ -63,6 +66,13 @@ func (feast *FeastServices) setOpenshiftTls() error { }, } } + if feast.onlineGrpcOpenshiftTls() { + appliedServices.OnlineStore.Grpc.TLS = &feastdevv1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: feast.initFeastSvc(OnlineGrpcFeastType).Name + tlsNameSuffix, + }, + } + } if feast.uiOpenshiftTls() { appliedServices.UI.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ @@ -114,7 +124,11 @@ func (feast *FeastServices) setOpenshiftTls() error { } func (feast *FeastServices) checkOpenshiftTls() (bool, error) { - if feast.offlineOpenshiftTls() || feast.onlineOpenshiftTls() || feast.localRegistryOpenshiftTls() || feast.uiOpenshiftTls() { + if feast.offlineOpenshiftTls() || + feast.onlineOpenshiftTls() || + feast.onlineGrpcOpenshiftTls() || + feast.localRegistryOpenshiftTls() || + feast.uiOpenshiftTls() { return true, nil } return feast.remoteRegistryOpenshiftTls() @@ -126,6 +140,8 @@ func (feast *FeastServices) isOpenShiftTls(feastType FeastServiceType) (isOpenSh isOpenShift = feast.offlineOpenshiftTls() case OnlineFeastType: isOpenShift = feast.onlineOpenshiftTls() + case OnlineGrpcFeastType: + isOpenShift = feast.onlineGrpcOpenshiftTls() case RegistryFeastType: isOpenShift = feast.localRegistryOpenshiftTls() case UIFeastType: @@ -136,6 +152,12 @@ func (feast *FeastServices) isOpenShiftTls(feastType FeastServiceType) (isOpenSh } func (feast *FeastServices) getTlsConfigs(feastType FeastServiceType) *feastdevv1.TlsConfigs { + if feastType == OnlineGrpcFeastType { + if grpcCfg := feast.getOnlineGrpcConfigs(); grpcCfg != nil { + return grpcCfg.TLS + } + return nil + } if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.TLS } @@ -158,6 +180,16 @@ func (feast *FeastServices) onlineOpenshiftTls() bool { feast.Handler.FeatureStore.Spec.Services.OnlineStore.Server.TLS == nil) } +// True if running in an openshift cluster and gRPC TLS not configured in the service Spec +func (feast *FeastServices) onlineGrpcOpenshiftTls() bool { + return isOpenShift && + feast.isOnlineGrpcServer() && + (feast.Handler.FeatureStore.Spec.Services == nil || + feast.Handler.FeatureStore.Spec.Services.OnlineStore == nil || + feast.Handler.FeatureStore.Spec.Services.OnlineStore.Grpc == nil || + feast.Handler.FeatureStore.Spec.Services.OnlineStore.Grpc.TLS == nil) +} + // True if running in an openshift cluster and Tls not configured in the service Spec func (feast *FeastServices) uiOpenshiftTls() bool { return isOpenShift && @@ -208,6 +240,7 @@ func (feast *FeastServices) mountTlsConfigs(podSpec *corev1.PodSpec) { feast.mountRegistryClientTls(podSpec) feast.mountTlsConfig(OfflineFeastType, podSpec) feast.mountTlsConfig(OnlineFeastType, podSpec) + feast.mountTlsConfig(OnlineGrpcFeastType, podSpec) feast.mountTlsConfig(UIFeastType, podSpec) feast.mountCustomCABundle(podSpec) } From c288dcec52e0ed85758aceb8f1d495ab96ac04d6 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 00:28:15 +0100 Subject: [PATCH 10/24] chore: retrigger CI Signed-off-by: Jatin Kumar From 06330e2e0ea49e4eae7c8e50760070813c2dbf84 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 00:41:31 +0100 Subject: [PATCH 11/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/featurestore_controller_test.go | 2 +- .../internal/controller/services/services_test.go | 4 ++-- infra/feast-operator/internal/controller/services/util.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 0dd1299f9f4..3b61cd9364a 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -1097,7 +1097,7 @@ var _ = Describe("FeatureStore Controller", func() { Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeTrue()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeNil()) Expect(resource.Status.ServiceHostnames.Registry).ToNot(BeEmpty()) Expect(resource.Status.ServiceHostnames.Registry).To(Equal(referencedRegistry.Status.ServiceHostnames.Registry)) feast := services.FeastServices{ diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 6487ceffb7f..f55ad20eaba 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -470,7 +470,7 @@ var _ = Describe("Registry Service", func() { // create online deployment Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - Expect(apimeta.IsStatusConditionTrue(featureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(feast.Handler.FeatureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) onlineDeploy := feast.initFeastDeploy(OnlineFeastType) Expect(onlineDeploy).NotTo(BeNil()) @@ -489,7 +489,7 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) - Expect(apimeta.FindStatusCondition(featureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeNil()) + Expect(apimeta.FindStatusCondition(feast.Handler.FeatureStore.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeNil()) err := k8sClient.Get(ctx, types.NamespacedName{ Name: onlineDeploy.Name, diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index e98449f4321..e4423132371 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -485,9 +485,9 @@ func GetOnlineContainer(deployment appsv1.Deployment) *corev1.Container { } func getContainerByType(feastType FeastServiceType, podSpec corev1.PodSpec) (int, *corev1.Container) { - for i, c := range podSpec.Containers { - if c.Name == string(feastType) { - return i, &c + for i := range podSpec.Containers { + if podSpec.Containers[i].Name == string(feastType) { + return i, &podSpec.Containers[i] } } return -1, nil From 99b5ca722860eaadb33976bce3335983bcda0868 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 00:50:06 +0100 Subject: [PATCH 12/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/featurestore_controller_test.go | 2 +- .../internal/controller/services/services_test.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 3b61cd9364a..2eaf4c97070 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -1189,7 +1189,7 @@ var _ = Describe("FeatureStore Controller", func() { Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.ReadyType)).To(BeFalse()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeTrue()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeNil()) Expect(resource.Status.Applied.Services.Registry.Remote.FeastRef.Name).To(Equal(referencedRegistry.Name)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index f55ad20eaba..a564c9d59a2 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -42,12 +42,10 @@ func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.N } func applySpecToStatus(fs *feastdevv1.FeatureStore) { - if fs.Status.Applied.Services == nil && fs.Spec.Services != nil { + if fs.Spec.Services != nil { fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() } - if len(fs.Status.Applied.FeastProject) == 0 { - fs.Status.Applied.FeastProject = fs.Spec.FeastProject - } + fs.Status.Applied.FeastProject = fs.Spec.FeastProject Expect(k8sClient.Status().Update(context.Background(), fs)).To(Succeed()) } From 81350f611b88a64adca4ca99f719f8e477c08f47 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 00:58:26 +0100 Subject: [PATCH 13/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../controller/services/services_test.go | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index a564c9d59a2..65e7f0e3f80 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -499,17 +499,21 @@ var _ = Describe("Registry Service", func() { Describe("Online gRPC TLS", func() { It("should mount TLS secret and configure command flags", func() { - featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ - Grpc: &feastdevv1.GrpcServerConfigs{ - TLS: &feastdevv1.TlsConfigs{ - SecretRef: &corev1.LocalObjectReference{ - Name: "grpc-tls", - }, + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Grpc: &feastdevv1.GrpcServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: "grpc-tls", + }, + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsKey: "tls.key", + TlsCrt: "tls.crt", + }, + }, + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), }, - ContainerConfigs: feastdevv1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), - }, }, }, } From 004552f3f4ac93fc8ef471fee1999ae746c0d614 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 01:06:13 +0100 Subject: [PATCH 14/24] chore: fixed format issue Signed-off-by: Jatin Kumar --- .../controller/services/services_test.go | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 65e7f0e3f80..4884c1fa4a3 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -499,21 +499,21 @@ var _ = Describe("Registry Service", func() { Describe("Online gRPC TLS", func() { It("should mount TLS secret and configure command flags", func() { - featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ - Grpc: &feastdevv1.GrpcServerConfigs{ - TLS: &feastdevv1.TlsConfigs{ - SecretRef: &corev1.LocalObjectReference{ - Name: "grpc-tls", - }, - SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsKey: "tls.key", - TlsCrt: "tls.crt", - }, - }, - ContainerConfigs: feastdevv1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Grpc: &feastdevv1.GrpcServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: "grpc-tls", + }, + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsKey: "tls.key", + TlsCrt: "tls.crt", + }, }, + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, }, }, } From 918aab0a7274023b781aec16641479b21e1a5a3d Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 01:10:04 +0100 Subject: [PATCH 15/24] chore: fixed devin ai comments Signed-off-by: Jatin Kumar --- .../internal/controller/services/services.go | 6 +++--- .../internal/controller/services/util.go | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 52555e9c47f..40bd17430e6 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -452,7 +452,7 @@ func (feast *FeastServices) setPod(podSpec *corev1.PodSpec, feastType FeastServi } feast.mountTlsConfigs(podSpec) feast.mountPvcConfigs(podSpec, feastType) - feast.mountEmptyDirVolumes(podSpec) + feast.mountEmptyDirVolumes(podSpec, feastType) feast.mountUserDefinedVolumes(podSpec) feast.applyNodeSelector(podSpec, feastType) @@ -1389,8 +1389,8 @@ func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *f } } -func (feast *FeastServices) mountEmptyDirVolumes(podSpec *corev1.PodSpec) { - if shouldMountEmptyDir(feast.Handler.FeatureStore) { +func (feast *FeastServices) mountEmptyDirVolumes(podSpec *corev1.PodSpec, feastType FeastServiceType) { + if shouldMountEmptyDir(feast.Handler.FeatureStore, feastType) { mountEmptyDirVolume(podSpec) } } diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index e4423132371..883060d55cc 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -73,13 +73,9 @@ func shouldCreatePvc(featureStore *feastdevv1.FeatureStore, feastType FeastServi return nil, false } -func shouldMountEmptyDir(featureStore *feastdevv1.FeatureStore) bool { - for _, feastType := range feastServerTypes { - if _, ok := hasPvcConfig(featureStore, feastType); !ok { - return true - } - } - return false +func shouldMountEmptyDir(featureStore *feastdevv1.FeatureStore, feastType FeastServiceType) bool { + _, ok := hasPvcConfig(featureStore, feastType) + return !ok } func getOfflineMountPath(featureStore *feastdevv1.FeatureStore) string { From cc6b34b548e24d4b70508e9c5a47155c72d578fa Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 01:15:46 +0100 Subject: [PATCH 16/24] chore: remove unused variables Signed-off-by: Jatin Kumar --- .../internal/controller/services/services_types.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index eab539b0630..32c70441d4d 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -233,14 +233,6 @@ var ( OidcProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl, OidcClientSecret, OidcUsername, OidcPassword} ) -// Feast server types: Reserved only for server types like Online, Offline, and Registry servers. Should not be used for client types like the UI, etc. -var feastServerTypes = []FeastServiceType{ - RegistryFeastType, - OfflineFeastType, - OnlineFeastType, - OnlineGrpcFeastType, -} - // AuthzType defines the authorization type type AuthzType string From 44cc01579a2f2388197e9bf33c6b98134169489d Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 01:21:17 +0100 Subject: [PATCH 17/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../controller/featurestore_controller_pvc_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index f6c68df8d07..15d5a89ffd3 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -402,13 +402,13 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { registryDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(registryDeploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) - Expect(services.GetRegistryContainer(*registryDeploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) + Expect(registryDeploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) + Expect(services.GetRegistryContainer(*registryDeploy).VolumeMounts).NotTo(ContainElement(ephemeralVolMount)) offlineDeploy, err = getDeploymentByType(ctx, k8sClient, resource, services.OfflineFeastType) Expect(err).NotTo(HaveOccurred()) - Expect(offlineDeploy.Spec.Template.Spec.Volumes).To(ContainElement(ephemeralVolume)) - Expect(services.GetOfflineContainer(*offlineDeploy).VolumeMounts).To(ContainElement(ephemeralVolMount)) + Expect(offlineDeploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(ephemeralVolume)) + Expect(services.GetOfflineContainer(*offlineDeploy).VolumeMounts).NotTo(ContainElement(ephemeralVolMount)) // check online pvc is deleted log.FromContext(feast.Handler.Context).Info("Checking deletion of", "PersistentVolumeClaim", onlineDeploy.Name) From 6cdc2fa4bc1d4c432981ae247fed6db9961c1e28 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 01:35:35 +0100 Subject: [PATCH 18/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../featurestore_controller_pvc_test.go | 6 ++++++ .../internal/controller/services/services.go | 16 ++++++++-------- .../internal/controller/services/util.go | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index 15d5a89ffd3..c3a58573596 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -313,6 +313,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(offlineVolMount.MountPath).To(Equal(offlineStoreMountPath)) Expect(offlineVolMount.Name).To(Equal(offlinePvcName)) assertEnvFrom(*offlineContainer) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers[0].WorkingDir).To(Equal(offlineStoreMountPath)) + Expect(offlineContainer.WorkingDir).To(Equal(path.Join(offlineStoreMountPath, resource.Spec.FeastProject, services.FeatureRepoDir))) // check offline pvc pvc := &corev1.PersistentVolumeClaim{} @@ -338,6 +341,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(onlineVolMount.MountPath).To(Equal(onlineStoreMountPath)) Expect(onlineVolMount.Name).To(Equal(onlinePvcName)) assertEnvFrom(*onlineContainer) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers[0].WorkingDir).To(Equal(onlineStoreMountPath)) + Expect(onlineContainer.WorkingDir).To(Equal(path.Join(onlineStoreMountPath, resource.Spec.FeastProject, services.FeatureRepoDir))) // check online pvc pvc = &corev1.PersistentVolumeClaim{} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 40bd17430e6..81583b6a20a 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -465,7 +465,7 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec, feastType Fea return err } - feast.setInitContainer(podSpec, fsYamlB64) + feast.setInitContainer(podSpec, feastType, fsYamlB64) feast.setContainer(&podSpec.Containers, feastType, fsYamlB64) return nil } @@ -477,7 +477,7 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy return } name := string(feastType) - workingDir := feast.getFeatureRepoDir() + workingDir := feast.getFeatureRepoDir(feastType) cmd := feast.getGrpcContainerCommand() container := getContainer(name, workingDir, cmd, grpcCfg.ContainerConfigs, fsYamlB64) container.Ports = []corev1.ContainerPort{ @@ -511,7 +511,7 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy } if serverConfigs := feast.getServerConfigs(feastType); serverConfigs != nil { name := string(feastType) - workingDir := feast.getFeatureRepoDir() + workingDir := feast.getFeatureRepoDir(feastType) cmd := feast.getContainerCommand(feastType) container := getContainer(name, workingDir, cmd, serverConfigs.ContainerConfigs, fsYamlB64) tls := feast.getTlsConfigs(feastType) @@ -733,11 +733,11 @@ func (feast *FeastServices) getDeploymentStrategy() appsv1.DeploymentStrategy { } } -func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 string) { +func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, feastType FeastServiceType, fsYamlB64 string) { applied := feast.Handler.FeatureStore.Status.Applied if applied.FeastProjectDir != nil && !applied.Services.DisableInitContainers { feastProjectDir := applied.FeastProjectDir - workingDir := getOfflineMountPath(feast.Handler.FeatureStore) + workingDir := getMountPath(feast.Handler.FeatureStore, feastType) projectPath := workingDir + "/" + applied.FeastProject container := corev1.Container{ Name: "feast-init", @@ -783,7 +783,7 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 } } - featureRepoDir := feast.getFeatureRepoDir() + featureRepoDir := feast.getFeatureRepoDir(feastType) container.Args = []string{ "echo \"Creating feast repository...\"\necho '" + createCommand + "'\n" + "if [[ ! -d " + featureRepoDir + " ]]; then " + createCommand + "; fi;\n" + @@ -1395,9 +1395,9 @@ func (feast *FeastServices) mountEmptyDirVolumes(podSpec *corev1.PodSpec, feastT } } -func (feast *FeastServices) getFeatureRepoDir() string { +func (feast *FeastServices) getFeatureRepoDir(feastType FeastServiceType) string { applied := feast.Handler.FeatureStore.Status.Applied - feastProjectDir := getOfflineMountPath(feast.Handler.FeatureStore) + "/" + applied.FeastProject + feastProjectDir := getMountPath(feast.Handler.FeatureStore, feastType) + "/" + applied.FeastProject if applied.FeastProjectDir != nil && applied.FeastProjectDir.Git != nil && len(applied.FeastProjectDir.Git.FeatureRepoPath) > 0 { return feastProjectDir + "/" + applied.FeastProjectDir.Git.FeatureRepoPath } diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 883060d55cc..fdea2c54085 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -78,8 +78,8 @@ func shouldMountEmptyDir(featureStore *feastdevv1.FeatureStore, feastType FeastS return !ok } -func getOfflineMountPath(featureStore *feastdevv1.FeatureStore) string { - if pvcConfig, ok := hasPvcConfig(featureStore, OfflineFeastType); ok { +func getMountPath(featureStore *feastdevv1.FeatureStore, feastType FeastServiceType) string { + if pvcConfig, ok := hasPvcConfig(featureStore, feastType); ok { return pvcConfig.MountPath } return EphemeralPath From d90e9f6a25a4c484c5ddcb3be8901e0bcc9c1dda Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 13:30:35 +0100 Subject: [PATCH 19/24] chore: addressed devin ai comments Signed-off-by: Jatin Kumar --- .../featurestore_controller_pvc_test.go | 14 ++++ .../internal/controller/services/services.go | 25 +++++--- .../controller/services/services_test.go | 64 +++++++++++++++++++ 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index c3a58573596..7432a1fad75 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -315,6 +315,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { assertEnvFrom(*offlineContainer) Expect(offlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) Expect(offlineDeploy.Spec.Template.Spec.InitContainers[0].WorkingDir).To(Equal(offlineStoreMountPath)) + offlineInitVolMount := services.GetOfflineVolumeMount(feast.Handler.FeatureStore, offlineDeploy.Spec.Template.Spec.InitContainers[0].VolumeMounts) + Expect(offlineInitVolMount).NotTo(BeNil()) + Expect(offlineInitVolMount.MountPath).To(Equal(offlineStoreMountPath)) + Expect(offlineInitVolMount.Name).To(Equal(offlinePvcName)) Expect(offlineContainer.WorkingDir).To(Equal(path.Join(offlineStoreMountPath, resource.Spec.FeastProject, services.FeatureRepoDir))) // check offline pvc @@ -343,6 +347,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { assertEnvFrom(*onlineContainer) Expect(onlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) Expect(onlineDeploy.Spec.Template.Spec.InitContainers[0].WorkingDir).To(Equal(onlineStoreMountPath)) + onlineInitVolMount := services.GetOnlineVolumeMount(feast.Handler.FeatureStore, onlineDeploy.Spec.Template.Spec.InitContainers[0].VolumeMounts) + Expect(onlineInitVolMount).NotTo(BeNil()) + Expect(onlineInitVolMount.MountPath).To(Equal(onlineStoreMountPath)) + Expect(onlineInitVolMount.Name).To(Equal(onlinePvcName)) Expect(onlineContainer.WorkingDir).To(Equal(path.Join(onlineStoreMountPath, resource.Spec.FeastProject, services.FeatureRepoDir))) // check online pvc @@ -368,6 +376,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { registryVolMount := services.GetRegistryVolumeMount(feast.Handler.FeatureStore, registryContainer.VolumeMounts) Expect(registryVolMount.MountPath).To(Equal(registryMountPath)) Expect(registryVolMount.Name).To(Equal(registryPvcName)) + Expect(registryDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(registryDeploy.Spec.Template.Spec.InitContainers[0].WorkingDir).To(Equal(registryMountPath)) + registryInitVolMount := services.GetRegistryVolumeMount(feast.Handler.FeatureStore, registryDeploy.Spec.Template.Spec.InitContainers[0].VolumeMounts) + Expect(registryInitVolMount).NotTo(BeNil()) + Expect(registryInitVolMount.MountPath).To(Equal(registryMountPath)) + Expect(registryInitVolMount.Name).To(Equal(registryPvcName)) // check registry pvc pvc = &corev1.PersistentVolumeClaim{} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 81583b6a20a..e3db90aa3f3 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -921,7 +921,7 @@ func (feast *FeastServices) setServiceAccount(sa *corev1.ServiceAccount) error { } func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1.PvcCreate, feastType FeastServiceType) (*corev1.PersistentVolumeClaim, error) { - pvc := feast.initPVC(feastType) + pvc := feast.initPVC(feast.pvcFeastType(feastType)) pvc.Spec = corev1.PersistentVolumeClaimSpec{ AccessModes: pvcCreate.AccessModes, @@ -1324,6 +1324,15 @@ func (feast *FeastServices) initPVC(feastType FeastServiceType) *corev1.Persiste return pvc } +func (feast *FeastServices) pvcFeastType(feastType FeastServiceType) FeastServiceType { + if feastType == OnlineGrpcFeastType { + if _, ok := hasPvcConfig(feast.Handler.FeatureStore, OnlineFeastType); ok { + return OnlineFeastType + } + } + return feastType +} + func (feast *FeastServices) initRoute(feastType FeastServiceType) *routev1.Route { route := &routev1.Route{ ObjectMeta: feast.GetObjectMetaType(feastType), @@ -1359,7 +1368,7 @@ func (feast *FeastServices) mountPvcConfigs(podSpec *corev1.PodSpec, feastType F func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *feastdevv1.PvcConfig, feastType FeastServiceType) { if podSpec != nil && pvcConfig != nil { - volName := feast.initPVC(feastType).Name + volName := feast.initPVC(feast.pvcFeastType(feastType)).Name pvcName := volName if pvcConfig.Ref != nil { pvcName = pvcConfig.Ref.Name @@ -1372,13 +1381,11 @@ func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *f }, }, }) - if feastType == OfflineFeastType { - for i := range podSpec.InitContainers { - podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: pvcConfig.MountPath, - }) - } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: pvcConfig.MountPath, + }) } for i := range podSpec.Containers { podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 4884c1fa4a3..3514b2e2754 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -35,6 +35,15 @@ func ptr[T any](v T) *T { return &v } +func findPvcVolume(volumes []corev1.Volume) *corev1.Volume { + for i := range volumes { + if volumes[i].PersistentVolumeClaim != nil { + return &volumes[i] + } + } + return nil +} + func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.NamespacedName) { fs := &feastdevv1.FeatureStore{} Expect(k8sClient.Get(ctx, key, fs)).To(Succeed()) @@ -497,6 +506,61 @@ var _ = Describe("Registry Service", func() { }) }) + Describe("Online gRPC PVC", func() { + It("should mount the same PVC as the online deployment when file persistence is enabled", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, + Grpc: &feastdevv1.GrpcServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + Path: "/data/online.db", + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, + MountPath: "/data", + }, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + onlineDeploy := feast.initFeastDeploy(OnlineFeastType) + Expect(onlineDeploy).NotTo(BeNil()) + Expect(feast.setDeployment(onlineDeploy, OnlineFeastType)).To(Succeed()) + + grpcDeploy := feast.initFeastDeploy(OnlineGrpcFeastType) + Expect(grpcDeploy).NotTo(BeNil()) + Expect(feast.setDeployment(grpcDeploy, OnlineGrpcFeastType)).To(Succeed()) + + onlinePvcVolume := findPvcVolume(onlineDeploy.Spec.Template.Spec.Volumes) + grpcPvcVolume := findPvcVolume(grpcDeploy.Spec.Template.Spec.Volumes) + + Expect(onlinePvcVolume).NotTo(BeNil()) + Expect(grpcPvcVolume).NotTo(BeNil()) + + expectedPvcName := feast.GetFeastServiceName(OnlineFeastType) + Expect(onlinePvcVolume.Name).To(Equal(expectedPvcName)) + Expect(onlinePvcVolume.PersistentVolumeClaim.ClaimName).To(Equal(expectedPvcName)) + Expect(grpcPvcVolume.Name).To(Equal(expectedPvcName)) + Expect(grpcPvcVolume.PersistentVolumeClaim.ClaimName).To(Equal(expectedPvcName)) + }) + }) + Describe("Online gRPC TLS", func() { It("should mount TLS secret and configure command flags", func() { featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ From b2d90fe71425a533ffec47ff93edbfb246fbc882 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 14:06:42 +0100 Subject: [PATCH 20/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/services/services_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 3514b2e2754..47d76000dd8 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -525,7 +525,7 @@ var _ = Describe("Registry Service", func() { }, Persistence: &feastdevv1.OnlineStorePersistence{ FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ - Path: "/data/online.db", + Path: "online.db", PvcConfig: &feastdevv1.PvcConfig{ Create: &feastdevv1.PvcCreate{}, MountPath: "/data", From 5488be8ad157be367c4e1a56223807ca193275d3 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 14:24:45 +0100 Subject: [PATCH 21/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/services/services.go | 24 +++++++-- .../controller/services/services_test.go | 51 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index e3db90aa3f3..7b1fb27afbf 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -246,7 +246,7 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) return feast.setFeastServiceCondition(err, feastType) } } else { - _ = feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)) + _ = feast.deletePVCForType(feastType) } if hasServerConfig { // For registry service, handle both gRPC and REST services @@ -322,7 +322,7 @@ func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) return err } } - if err := feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { + if err := feast.deletePVCForType(feastType); err != nil { return err } apimeta.RemoveStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) @@ -1326,13 +1326,27 @@ func (feast *FeastServices) initPVC(feastType FeastServiceType) *corev1.Persiste func (feast *FeastServices) pvcFeastType(feastType FeastServiceType) FeastServiceType { if feastType == OnlineGrpcFeastType { - if _, ok := hasPvcConfig(feast.Handler.FeatureStore, OnlineFeastType); ok { - return OnlineFeastType - } + return OnlineFeastType } return feastType } +func (feast *FeastServices) shouldDeletePVCForType(feastType FeastServiceType) bool { + if feastType == OnlineGrpcFeastType { + if _, ok := hasPvcConfig(feast.Handler.FeatureStore, OnlineFeastType); ok && feast.isOnlineServer() { + return false + } + } + return true +} + +func (feast *FeastServices) deletePVCForType(feastType FeastServiceType) error { + if !feast.shouldDeletePVCForType(feastType) { + return nil + } + return feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feast.pvcFeastType(feastType))) +} + func (feast *FeastServices) initRoute(feastType FeastServiceType) *routev1.Route { route := &routev1.Route{ ObjectMeta: feast.GetObjectMetaType(feastType), diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 47d76000dd8..1821ae2f197 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -561,6 +561,57 @@ var _ = Describe("Registry Service", func() { }) }) + Describe("Online gRPC PVC cleanup", func() { + It("should delete the shared online PVC when the gRPC service is disabled", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Grpc: &feastdevv1.GrpcServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr("test-image"), + }, + }, + }, + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + Path: "online.db", + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, + MountPath: "/data", + }, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineGrpcFeastType)).To(Succeed()) + + pvcName := feast.GetFeastServiceName(OnlineFeastType) + pvc := &corev1.PersistentVolumeClaim{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: pvcName, + Namespace: featureStore.Namespace, + }, pvc)).To(Succeed()) + + featureStore.Spec.Services.OnlineStore = nil + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.reconcileOnlineGrpc()).To(Succeed()) + + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: pvcName, + Namespace: featureStore.Namespace, + }, pvc) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + Describe("Online gRPC TLS", func() { It("should mount TLS secret and configure command flags", func() { featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ From af3dbae4aa0f501010028f7cadda641d408bd5ae Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 14:35:18 +0100 Subject: [PATCH 22/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/services/services_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 1821ae2f197..323a997b02b 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -26,6 +26,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -527,7 +528,14 @@ var _ = Describe("Registry Service", func() { FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: "online.db", PvcConfig: &feastdevv1.PvcConfig{ - Create: &feastdevv1.PvcCreate{}, + Create: &feastdevv1.PvcCreate{ + AccessModes: DefaultPVCAccessModes, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(DefaultOnlineStorageRequest), + }, + }, + }, MountPath: "/data", }, }, From 4ce3f2641c0ab072f37b761c64d69497f1d0d5f2 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 14:51:06 +0100 Subject: [PATCH 23/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/services/services.go | 5 +++++ .../internal/controller/services/services_test.go | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 7b1fb27afbf..1346be1d4d8 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -1337,6 +1337,11 @@ func (feast *FeastServices) shouldDeletePVCForType(feastType FeastServiceType) b return false } } + if feastType == OnlineFeastType { + if _, ok := hasPvcConfig(feast.Handler.FeatureStore, OnlineFeastType); ok && feast.isOnlineGrpcServer() { + return false + } + } return true } diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 323a997b02b..5bd753ca15b 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -583,7 +583,14 @@ var _ = Describe("Registry Service", func() { FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: "online.db", PvcConfig: &feastdevv1.PvcConfig{ - Create: &feastdevv1.PvcCreate{}, + Create: &feastdevv1.PvcCreate{ + AccessModes: DefaultPVCAccessModes, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(DefaultOnlineStorageRequest), + }, + }, + }, MountPath: "/data", }, }, From 02bede90974d39d7eef7978a18a4c08acbc11240 Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Tue, 3 Feb 2026 15:00:56 +0100 Subject: [PATCH 24/24] chore: fixed test cases Signed-off-by: Jatin Kumar --- .../internal/controller/services/services_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 5bd753ca15b..ed026c6762e 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -623,7 +623,11 @@ var _ = Describe("Registry Service", func() { Name: pvcName, Namespace: featureStore.Namespace, }, pvc) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) + if err != nil { + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } else { + Expect(pvc.DeletionTimestamp).NotTo(BeNil()) + } }) })