diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index c64c26ce020..8c998686dcf 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -30,14 +30,15 @@ const ( FailedPhase = "Failed" // Feast condition types: - ClientReadyType = "Client" - OfflineStoreReadyType = "OfflineStore" - OnlineStoreReadyType = "OnlineStore" - RegistryReadyType = "Registry" - UIReadyType = "UI" - ReadyType = "FeatureStore" - AuthorizationReadyType = "Authorization" - CronJobReadyType = "CronJob" + ClientReadyType = "Client" + OfflineStoreReadyType = "OfflineStore" + OnlineStoreReadyType = "OnlineStore" + OnlineStoreGrpcReadyType = "OnlineStoreGrpc" + RegistryReadyType = "Registry" + UIReadyType = "UI" + ReadyType = "FeatureStore" + AuthorizationReadyType = "Authorization" + CronJobReadyType = "CronJob" // Feast condition reasons: ReadyReason = "Ready" @@ -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,8 +76,44 @@ type FeatureStoreSpec struct { FeastProject string `json:"feastProject"` FeastProjectDir *FeastProjectDir `json:"feastProjectDir,omitempty"` Services *FeatureStoreServices `json:"services,omitempty"` - AuthzConfig *AuthzConfig `json:"authz,omitempty"` - CronJob *FeastCronJob `json:"cronJob,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. @@ -348,7 +387,9 @@ 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"` } @@ -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,23 @@ 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"` + // 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. + 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 { @@ -684,11 +744,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"` - Registry string `json:"registry,omitempty"` - RegistryRest string `json:"registryRest,omitempty"` - UI string `json:"ui,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"` } // +kubebuilder:object:root=true diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index 9e328bf5b6c..cb38154a224 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,54 @@ 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.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)) + 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 +817,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 +1196,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..dee504270f8 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. @@ -1606,8 +1517,7 @@ spec: type: object type: object tls: - description: TlsConfigs configures server TLS for a feast - service. + description: TLS configures TLS for the gRPC server. properties: disable: description: will disable TLS for the feast service. @@ -1645,7 +1555,7 @@ spec: : 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 +1597,72 @@ 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 - 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 + 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: - 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 +1673,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 +1785,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,259 +1850,162 @@ 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. + 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 + 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: - 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: "" + 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 @@ -2319,94 +2013,780 @@ 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. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by the containers in this pod. - Note that this field cannot be set when spec.os. + local: + description: LocalRegistryConfig configures the registry service properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. - type: string - type: + 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 + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: description: type indicates which kind of seccomp profile will be applied. type: string @@ -2643,6 +3023,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. @@ -4257,59 +4642,454 @@ spec: description: The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. items: - type: string + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + 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: + - secretRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + 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 + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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 type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. 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 + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. type: object - x-kubernetes-map-type: atomic - required: - - secretRef + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' - cronJob: - description: FeastCronJob defines a CronJob to execute against - a Feature Store deployment. + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. properties: - annotations: - additionalProperties: - type: string - description: Annotations to be added to the CronJob metadata. - type: object - concurrencyPolicy: - description: Specifies how to treat concurrent executions - of a Job. - type: string - containerConfigs: - description: CronJobContainerConfigs k8s container settings - for the CronJob + git: + description: GitCloneOptions describes how a clone should + be performed. properties: - commands: - description: Array of commands to be executed (in order) - against a Feature Store deployment. - items: + configs: + additionalProperties: type: string - type: array + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object env: items: description: EnvVar represents an environment variable @@ -4463,273 +5243,404 @@ spec: x-kubernetes-map-type: atomic type: object type: array - image: + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image + ref: + description: Reference to a branch / tag / commit type: string - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. + 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: - 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 + maxSurge: + anyOf: + - type: integer + - type: string description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. + 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 - 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. + 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 - type: object - failedJobsHistoryLimit: - description: The number of failed finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - jobSpec: - description: Specification of the desired behavior of a job. - properties: - activeDeadlineSeconds: - description: |- - Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr - format: int64 - type: integer - backoffLimit: - description: Specifies the number of retries before marking - this job failed. - format: int32 - type: integer - backoffLimitPerIndex: - description: |- - Specifies the limit for the number of retries within an - index before marking this index as failed. - format: int32 - type: integer - completionMode: - description: |- - completionMode specifies how Pod completions are tracked. It can be - `NonIndexed` (default) or `Indexed`. - type: string - completions: - description: |- - Specifies the desired number of successfully finished pods the - job should be run with. - format: int32 - type: integer - maxFailedIndexes: - description: |- - Specifies the maximal number of failed indexes before marking the Job as - failed, when backoffLimitPerIndex is set. - format: int32 - type: integer - parallelism: - description: |- - Specifies the maximum desired number of pods the job should - run at any given time. - format: int32 - type: integer - podFailurePolicy: - description: Specifies the policy of handling failed pods. + 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: - rules: - description: A list of pod failure policy rules. The - rules are evaluated in order. + env: items: - description: PodFailurePolicyRule describes how - a pod failure is handled when the requirements - are met. + description: EnvVar represents an environment variable + present in a Container. properties: - action: - description: Specifies the action taken on a - pod failure when the requirements are satisfied. + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. type: string - onExitCodes: - description: Represents the requirement on the - container exit codes. + 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: - containerName: - description: |- - Restricts the check for exit codes to the container with the - specified name. - type: string - operator: + 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: |- - Represents the relationship between the container exit code(s) and the - specified values. - type: string - values: - description: Specifies the set of values. - items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - - values + 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 - onPodConditions: - description: |- - Represents the requirement on the pod conditions. The requirement is represented - as a list of pod condition patterns. - items: - description: |- - PodFailurePolicyOnPodConditionsPattern describes a pattern for matching - an actual pod condition type. - properties: - status: - description: Specifies the required Pod - condition status. - type: string - type: - description: Specifies the required Pod - condition type. - type: string - required: - - status - - type - type: object - type: array - x-kubernetes-list-type: atomic required: - - action + - name type: object type: array - x-kubernetes-list-type: atomic - required: - - rules - type: object - podReplacementPolicy: - description: podReplacementPolicy specifies when to create - replacement Pods. - type: string - podTemplateAnnotations: - additionalProperties: - type: string - description: |- - PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate - metadata. - type: object - suspend: - description: suspend specifies whether the Job controller - should create Pods or not. - type: boolean - ttlSecondsAfterFinished: - description: |- - ttlSecondsAfterFinished limits the lifetime of a Job that has finished - execution (either Complete or Failed). - format: int32 - type: integer - type: object - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - startingDeadlineSeconds: - description: |- - Optional deadline in seconds for starting the job if it misses scheduled - time for any reason. - format: int64 - type: integer - successfulJobsHistoryLimit: - description: The number of successful finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - suspend: - description: |- - This flag tells the controller to suspend subsequent executions, it does - not apply to already started executions. - type: boolean - timeZone: - description: The time zone name for the given schedule, see - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - type: string - type: object - feastProject: - description: FeastProject is the Feast project id. - pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ - type: string - feastProjectDir: - description: FeastProjectDir defines how to create the feast project - directory. - properties: - git: - description: GitCloneOptions describes how a clone should - be performed. - properties: - configs: - additionalProperties: - type: string - description: |- - Configs passed to git via `-c` - e.g. http.sslVerify: 'false' - OR 'url."https://api:\${TOKEN}@github.com/". - type: object - 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. + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. + configMapRef: + description: The ConfigMap to select from properties: - key: - description: The key to select. - type: string name: default: "" description: |- @@ -4739,314 +5650,131 @@ spec: type: string optional: description: Specify whether the ConfigMap - or its key must be defined + 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. + 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: - 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' + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - required: - - resource + optional: + description: Specify whether the Secret + must be defined + type: boolean type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + 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: - 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. + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean required: - - key + - name 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 - 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 - 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: 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 +5785,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 +6052,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. @@ -5313,8 +6123,7 @@ spec: type: object type: object tls: - description: TlsConfigs configures server TLS for - a feast service. + description: TLS configures TLS for the gRPC server. properties: disable: description: will disable TLS for the feast service. @@ -5352,7 +6161,7 @@ spec: : 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 +6204,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 +6543,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 +7095,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 +7659,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 +9343,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/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b34092d8b42..5157ad4279b 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. @@ -1614,8 +1525,7 @@ spec: type: object type: object tls: - description: TlsConfigs configures server TLS for a feast - service. + description: TLS configures TLS for the gRPC server. properties: disable: description: will disable TLS for the feast service. @@ -1653,7 +1563,7 @@ spec: : 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 +1605,72 @@ 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 - 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 + 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: - 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 +1681,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 +1793,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,259 +1858,162 @@ 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. + 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 + 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: - 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: "" + 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 @@ -2327,94 +2021,780 @@ 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. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by the containers in this pod. - Note that this field cannot be set when spec.os. + local: + description: LocalRegistryConfig configures the registry service properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. - type: string - type: + 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 + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: description: type indicates which kind of seccomp profile will be applied. type: string @@ -2651,6 +3031,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. @@ -4265,59 +4650,454 @@ spec: description: The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. items: - type: string + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + 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: + - secretRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + 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 + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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 type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. 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 + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. type: object - x-kubernetes-map-type: atomic - required: - - secretRef + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' - cronJob: - description: FeastCronJob defines a CronJob to execute against - a Feature Store deployment. + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. properties: - annotations: - additionalProperties: - type: string - description: Annotations to be added to the CronJob metadata. - type: object - concurrencyPolicy: - description: Specifies how to treat concurrent executions - of a Job. - type: string - containerConfigs: - description: CronJobContainerConfigs k8s container settings - for the CronJob + git: + description: GitCloneOptions describes how a clone should + be performed. properties: - commands: - description: Array of commands to be executed (in order) - against a Feature Store deployment. - items: + configs: + additionalProperties: type: string - type: array + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object env: items: description: EnvVar represents an environment variable @@ -4471,273 +5251,404 @@ spec: x-kubernetes-map-type: atomic type: object type: array - image: + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image + ref: + description: Reference to a branch / tag / commit type: string - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. + 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: - 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 + maxSurge: + anyOf: + - type: integer + - type: string description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. + 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 - 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. + 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 - type: object - failedJobsHistoryLimit: - description: The number of failed finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - jobSpec: - description: Specification of the desired behavior of a job. - properties: - activeDeadlineSeconds: - description: |- - Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr - format: int64 - type: integer - backoffLimit: - description: Specifies the number of retries before marking - this job failed. - format: int32 - type: integer - backoffLimitPerIndex: - description: |- - Specifies the limit for the number of retries within an - index before marking this index as failed. - format: int32 - type: integer - completionMode: - description: |- - completionMode specifies how Pod completions are tracked. It can be - `NonIndexed` (default) or `Indexed`. - type: string - completions: - description: |- - Specifies the desired number of successfully finished pods the - job should be run with. - format: int32 - type: integer - maxFailedIndexes: - description: |- - Specifies the maximal number of failed indexes before marking the Job as - failed, when backoffLimitPerIndex is set. - format: int32 - type: integer - parallelism: - description: |- - Specifies the maximum desired number of pods the job should - run at any given time. - format: int32 - type: integer - podFailurePolicy: - description: Specifies the policy of handling failed pods. + 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: - rules: - description: A list of pod failure policy rules. The - rules are evaluated in order. + env: items: - description: PodFailurePolicyRule describes how - a pod failure is handled when the requirements - are met. + description: EnvVar represents an environment variable + present in a Container. properties: - action: - description: Specifies the action taken on a - pod failure when the requirements are satisfied. + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. type: string - onExitCodes: - description: Represents the requirement on the - container exit codes. + 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: - containerName: - description: |- - Restricts the check for exit codes to the container with the - specified name. - type: string - operator: + 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: |- - Represents the relationship between the container exit code(s) and the - specified values. - type: string - values: - description: Specifies the set of values. - items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - - values + 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 - onPodConditions: - description: |- - Represents the requirement on the pod conditions. The requirement is represented - as a list of pod condition patterns. - items: - description: |- - PodFailurePolicyOnPodConditionsPattern describes a pattern for matching - an actual pod condition type. - properties: - status: - description: Specifies the required Pod - condition status. - type: string - type: - description: Specifies the required Pod - condition type. - type: string - required: - - status - - type - type: object - type: array - x-kubernetes-list-type: atomic required: - - action + - name type: object type: array - x-kubernetes-list-type: atomic - required: - - rules - type: object - podReplacementPolicy: - description: podReplacementPolicy specifies when to create - replacement Pods. - type: string - podTemplateAnnotations: - additionalProperties: - type: string - description: |- - PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate - metadata. - type: object - suspend: - description: suspend specifies whether the Job controller - should create Pods or not. - type: boolean - ttlSecondsAfterFinished: - description: |- - ttlSecondsAfterFinished limits the lifetime of a Job that has finished - execution (either Complete or Failed). - format: int32 - type: integer - type: object - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - startingDeadlineSeconds: - description: |- - Optional deadline in seconds for starting the job if it misses scheduled - time for any reason. - format: int64 - type: integer - successfulJobsHistoryLimit: - description: The number of successful finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - suspend: - description: |- - This flag tells the controller to suspend subsequent executions, it does - not apply to already started executions. - type: boolean - timeZone: - description: The time zone name for the given schedule, see - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - type: string - type: object - feastProject: - description: FeastProject is the Feast project id. - pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ - type: string - feastProjectDir: - description: FeastProjectDir defines how to create the feast project - directory. - properties: - git: - description: GitCloneOptions describes how a clone should - be performed. - properties: - configs: - additionalProperties: - type: string - description: |- - Configs passed to git via `-c` - e.g. http.sslVerify: 'false' - OR 'url."https://api:\${TOKEN}@github.com/". - type: object - 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. + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. + configMapRef: + description: The ConfigMap to select from properties: - key: - description: The key to select. - type: string name: default: "" description: |- @@ -4747,314 +5658,131 @@ spec: type: string optional: description: Specify whether the ConfigMap - or its key must be defined + 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. + 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: - 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' + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - required: - - resource + optional: + description: Specify whether the Secret + must be defined + type: boolean type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + 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: - 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. + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean required: - - key + - name 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 - 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 - 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: 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 +5793,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 +6060,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. @@ -5321,8 +6131,7 @@ spec: type: object type: object tls: - description: TlsConfigs configures server TLS for - a feast service. + description: TLS configures TLS for the gRPC server. properties: disable: description: will disable TLS for the feast service. @@ -5360,7 +6169,7 @@ spec: : 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 +6212,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 +6551,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 +7103,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 +7667,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 +9351,8 @@ spec: type: string onlineStore: type: string + onlineStoreGrpc: + type: string registry: type: string registryRest: diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 64998542d2f..81bc9b08bed 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,31 @@ 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. | +| `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. | +| `registryTTLSeconds` _integer_ | RegistryTTLSeconds sets how often the registry is refreshed. | + + #### JobSpec @@ -505,6 +573,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 +632,7 @@ OptionalCtrConfigs k8s container settings that are optional _Appears in:_ - [ContainerConfigs](#containerconfigs) - [CronJobContainerConfigs](#cronjobcontainerconfigs) +- [GrpcServerConfigs](#grpcserverconfigs) - [RegistryServerConfigs](#registryserverconfigs) - [ServerConfigs](#serverconfigs) @@ -700,6 +770,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 +837,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 +862,7 @@ _Appears in:_ | --- | --- | | `offlineStore` _string_ | | | `onlineStore` _string_ | | +| `onlineStoreGrpc` _string_ | | | `registry` _string_ | | | `registryRest` _string_ | | | `ui` _string_ | | @@ -802,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/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_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..7432a1fad75 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -277,34 +277,49 @@ 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) + 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 pvc := &corev1.PersistentVolumeClaim{} @@ -319,19 +334,24 @@ 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) + 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 pvc = &corev1.PersistentVolumeClaim{} @@ -346,17 +366,22 @@ 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)) 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{} @@ -389,22 +414,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).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).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", 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 +476,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 +497,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 +536,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 +558,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 +637,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 +656,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 +674,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 bfd4a484cff..2eaf4c97070 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{ { @@ -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) @@ -485,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()) @@ -657,17 +633,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 +694,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 +721,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 +746,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 +769,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 +840,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 +885,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 +911,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 +933,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 +963,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 +982,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 +1003,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()) @@ -1116,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{ @@ -1138,12 +1119,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 +1159,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()) @@ -1218,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()) @@ -1300,21 +1271,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 +1344,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) @@ -1454,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)) }) 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/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..1346be1d4d8 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" @@ -51,91 +52,107 @@ 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 { + 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) +} - if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OnlineFeastType); err != nil { - return err - } +func (feast *FeastServices) reconcileOnlineGrpc() error { + if feast.isOnlineGrpcServer() { + 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.createServiceAccount(); err != nil { + if err := feast.removeFeastServiceByType(UIFeastType); err != nil { return err } - if err := feast.createDeployment(); err != nil { + return feast.removeRoute(UIFeastType) +} + +func (feast *FeastServices) deploySupportServices() error { + if err := feast.createServiceAccount(); err != nil { return err } if err := feast.deployClient(); err != nil { @@ -144,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 { @@ -221,14 +234,21 @@ 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) } } else { - _ = feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)) + _ = feast.deletePVCForType(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,8 +286,12 @@ 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)) + _ = feast.Handler.DeleteOwnedFeastObj(feast.initFeastDeploy(feastType)) // Delete REST API service if it exists _ = feast.Handler.DeleteOwnedFeastObj(&corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -275,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) } @@ -283,7 +309,20 @@ func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastSvc(feastType)); err != nil { return err } - if err := feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { + 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.deletePVCForType(feastType); err != nil { return err } apimeta.RemoveStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) @@ -327,11 +366,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 +418,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,51 +440,78 @@ 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.mountEmptyDirVolumes(podSpec) + feast.mountPvcConfigs(podSpec, feastType) + feast.mountEmptyDirVolumes(podSpec, feastType) 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.setInitContainer(podSpec, feastType, 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(feastType) + 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, feast.getTlsConfigs(feastType)) + 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() + workingDir := feast.getFeatureRepoDir(feastType) cmd := feast.getContainerCommand(feastType) container := getContainer(name, workingDir, cmd, serverConfigs.ContainerConfigs, fsYamlB64) tls := feast.getTlsConfigs(feastType) @@ -532,6 +601,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 +706,24 @@ 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))) + } + } + 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 +} + func (feast *FeastServices) getDeploymentStrategy() appsv1.DeploymentStrategy { if feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy != nil { return *feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy @@ -640,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", @@ -690,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" + @@ -702,6 +795,28 @@ 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() + 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, + 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 +872,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{ { @@ -806,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, @@ -829,6 +944,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 +956,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 +985,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 +1011,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 +1066,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 +1107,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 +1241,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 +1260,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 @@ -1156,6 +1324,34 @@ func (feast *FeastServices) initPVC(feastType FeastServiceType) *corev1.Persiste return pvc } +func (feast *FeastServices) pvcFeastType(feastType FeastServiceType) FeastServiceType { + if feastType == OnlineGrpcFeastType { + 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 + } + } + if feastType == OnlineFeastType { + if _, ok := hasPvcConfig(feast.Handler.FeatureStore, OnlineFeastType); ok && feast.isOnlineGrpcServer() { + 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), @@ -1183,17 +1379,15 @@ 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) } } 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 @@ -1206,13 +1400,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{ @@ -1223,15 +1415,15 @@ 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) } } -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 } @@ -1277,8 +1469,20 @@ 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 { + 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..ed026c6762e 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -24,6 +24,9 @@ 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" + "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" @@ -33,6 +36,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()) @@ -40,7 +52,9 @@ func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.N } func applySpecToStatus(fs *feastdevv1.FeatureStore) { - fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() + if fs.Spec.Services != nil { + fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() + } fs.Status.Applied.FeastProject = fs.Spec.FeastProject Expect(k8sClient.Status().Update(context.Background(), fs)).To(Succeed()) } @@ -63,6 +77,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", @@ -160,31 +176,31 @@ 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(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)) - 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))) }) It("should configure correct REST container ports", func() { setFeatureStoreServerConfig(false, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + 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)) - 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")) }) @@ -192,15 +208,15 @@ var _ = Describe("Registry Service", func() { setFeatureStoreServerConfig(true, true) Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) - deployment := feast.initFeastDeploy() + 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)) - 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")) }) }) @@ -219,9 +235,9 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify NodeSelector is applied - deployment := feast.initFeastDeploy() + 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 +247,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", @@ -263,15 +279,14 @@ 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()) + 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", + "node-type": "online", + "zone": "us-west-1a", } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -298,9 +313,9 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment first - deployment := feast.initFeastDeploy() + 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 +327,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 +338,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 +378,11 @@ var _ = Describe("Registry Service", func() { feast.refreshFeatureStore(ctx, typeNamespacedName) // Create deployment and verify UI service selector is applied - deployment := feast.initFeastDeploy() + 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", @@ -377,7 +392,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()) @@ -387,13 +409,13 @@ 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()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) 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, @@ -428,15 +450,248 @@ 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(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()) }) }) + 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(feast.Handler.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(feast.Handler.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 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: "online.db", + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{ + AccessModes: DefaultPVCAccessModes, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(DefaultOnlineStorageRequest), + }, + }, + }, + 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 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{ + AccessModes: DefaultPVCAccessModes, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(DefaultOnlineStorageRequest), + }, + }, + }, + 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) + if err != nil { + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } else { + Expect(pvc.DeletionTimestamp).NotTo(BeNil()) + } + }) + }) + + 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"), + }, + }, + }, + } + + 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 @@ -472,9 +727,9 @@ 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()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) @@ -520,9 +775,9 @@ 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()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) @@ -559,9 +814,9 @@ 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()) + Expect(feast.setDeployment(deployment, OnlineFeastType)).To(Succeed()) onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index d437c703a6a..32c70441d4d 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -55,15 +55,17 @@ const ( DefaultOnlineStorageRequest = "5Gi" DefaultRegistryStorageRequest = "5Gi" MetricsPort int32 = 8000 - - AuthzFeastType FeastServiceType = "authorization" - OfflineFeastType FeastServiceType = "offline" - OnlineFeastType FeastServiceType = "online" - RegistryFeastType FeastServiceType = "registry" - UIFeastType FeastServiceType = "ui" - ClientFeastType FeastServiceType = "client" - ClientCaFeastType FeastServiceType = "client-ca" - CronJobFeastType FeastServiceType = "cronjob" + 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" + ClientCaFeastType FeastServiceType = "client-ca" + CronJobFeastType FeastServiceType = "cronjob" OfflineRemoteConfigType OfflineConfigType = "remote" OfflineFilePersistenceDaskConfigType OfflineConfigType = "dask" @@ -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, @@ -213,13 +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, -} - // AuthzType defines the authorization type type AuthzType string @@ -249,13 +262,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"` - EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,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"` } // OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline 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) } diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index e5299d79119..00a956b9f81 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -170,16 +170,14 @@ var _ = Describe("TLS Config", func() { Expect(openshiftTls).To(BeTrue()) // check k8s deployment objects - feastDeploy := feast.initFeastDeploy() - err = feast.setDeployment(feastDeploy) + feastDeploy := feast.initFeastDeploy(OnlineFeastType) + 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() - 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() - 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"))) }) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 8e8a717aecf..fdea2c54085 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 { @@ -68,17 +73,13 @@ 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 { - 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 @@ -90,75 +91,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{} @@ -179,15 +205,40 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) } - if services.OnlineStore.Server == 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 onlineStore.Server != nil { + setDefaultCtrConfigs(&onlineStore.Server.ContainerConfigs.DefaultCtrConfigs) } - setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) + if onlineStore.Grpc != nil { + setDefaultCtrConfigs(&onlineStore.Grpc.ContainerConfigs.DefaultCtrConfigs) + if onlineStore.Grpc.Port == nil { + defaultPort := DefaultOnlineGrpcPort + onlineStore.Grpc.Port = &defaultPort + } + if onlineStore.Grpc.MaxWorkers == nil { + defaultMaxWorkers := int32(10) + onlineStore.Grpc.MaxWorkers = &defaultMaxWorkers + } + if onlineStore.Grpc.RegistryTTLSeconds == nil { + defaultRegistryTTL := int32(5) + 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{} } @@ -430,9 +481,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