From aa55a1a3286a6ad1c4f90ee1a38378af5267e3fb Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Tue, 22 Oct 2024 11:24:06 -0500 Subject: [PATCH] Add services functionality to operator Signed-off-by: Tommy Hughes --- .../api/feastversion/version.go | 2 +- .../api/v1alpha1/featurestore_types.go | 113 ++- .../api/v1alpha1/zz_generated.deepcopy.go | 229 ++++- .../feast-operator.clusterserviceversion.yaml | 2 +- .../manifests/feast.dev_featurestores.yaml | 502 ++++++++++- infra/feast-operator/cmd/main.go | 1 + .../crd/bases/feast.dev_featurestores.yaml | 502 ++++++++++- infra/feast-operator/dist/install.yaml | 502 ++++++++++- .../controller/featurestore_controller.go | 86 +- .../featurestore_controller_test.go | 820 +++++++++++++++++- .../internal/controller/services/client.go | 28 +- .../internal/controller/services/registry.go | 245 ------ .../controller/services/repo_config.go | 105 +++ .../internal/controller/services/services.go | 398 +++++++++ .../controller/services/services_types.go | 126 ++- infra/scripts/release/files_to_bump.txt | 2 +- 16 files changed, 3331 insertions(+), 332 deletions(-) delete mode 100644 infra/feast-operator/internal/controller/services/registry.go create mode 100644 infra/feast-operator/internal/controller/services/repo_config.go create mode 100644 infra/feast-operator/internal/controller/services/services.go diff --git a/infra/feast-operator/api/feastversion/version.go b/infra/feast-operator/api/feastversion/version.go index ac97cd03266..77a9db1d57f 100644 --- a/infra/feast-operator/api/feastversion/version.go +++ b/infra/feast-operator/api/feastversion/version.go @@ -17,4 +17,4 @@ limitations under the License. package feastversion // Feast release version -const FeastVersion = "0.40.0" +const FeastVersion = "0.41.0" diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 1afd7069f0b..ce2ebe37329 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,20 +28,26 @@ const ( FailedPhase = "Failed" // Feast condition types: - ClientReadyType = "Client" - RegistryReadyType = "Registry" - ReadyType = "FeatureStore" + ClientReadyType = "Client" + OfflineStoreReadyType = "OfflineStore" + OnlineStoreReadyType = "OnlineStore" + RegistryReadyType = "Registry" + ReadyType = "FeatureStore" // Feast condition reasons: - ReadyReason = "Ready" - FailedReason = "FeatureStoreFailed" - RegistryFailedReason = "RegistryDeploymentFailed" - ClientFailedReason = "ClientDeploymentFailed" + ReadyReason = "Ready" + FailedReason = "FeatureStoreFailed" + OfflineStoreFailedReason = "OfflineStoreDeploymentFailed" + OnlineStoreFailedReason = "OnlineStoreDeploymentFailed" + RegistryFailedReason = "RegistryDeploymentFailed" + ClientFailedReason = "ClientDeploymentFailed" // Feast condition messages: - ReadyMessage = "FeatureStore installation complete" - RegistryReadyMessage = "Registry installation complete" - ClientReadyMessage = "Client installation complete" + ReadyMessage = "FeatureStore installation complete" + OfflineStoreReadyMessage = "Offline Store installation complete" + OnlineStoreReadyMessage = "Online Store installation complete" + RegistryReadyMessage = "Registry installation complete" + ClientReadyMessage = "Client installation complete" // entity_key_serialization_version SerializationVersion = 3 @@ -50,22 +57,92 @@ const ( type FeatureStoreSpec struct { // +kubebuilder:validation:Pattern="^[A-Za-z0-9][A-Za-z0-9_]*$" // FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an underscore. Required. - FeastProject string `json:"feastProject"` + FeastProject string `json:"feastProject"` + Services *FeatureStoreServices `json:"services,omitempty"` +} + +// FeatureStoreServices defines the desired feast service deployments. ephemeral registry is deployed by default. +type FeatureStoreServices struct { + OfflineStore *OfflineStore `json:"offlineStore,omitempty"` + OnlineStore *OnlineStore `json:"onlineStore,omitempty"` + Registry *Registry `json:"registry,omitempty"` +} + +// OfflineStore configures the deployed offline store service +type OfflineStore struct { + ServiceConfigs `json:",inline"` +} + +// OnlineStore configures the deployed online store service +type OnlineStore struct { + ServiceConfigs `json:",inline"` +} + +// LocalRegistryConfig configures the deployed registry service +type LocalRegistryConfig struct { + ServiceConfigs `json:",inline"` +} + +// Registry configures the registry service. One selection is required. Local is the default setting. +// +kubebuilder:validation:XValidation:rule="[has(self.local), has(self.remote)].exists_one(c, c)",message="One selection required." +type Registry struct { + Local *LocalRegistryConfig `json:"local,omitempty"` + Remote *RemoteRegistryConfig `json:"remote,omitempty"` +} + +// RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. +// Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. +// +kubebuilder:validation:XValidation:rule="[has(self.hostname), has(self.feastRef)].exists_one(c, c)",message="One selection required." +type RemoteRegistryConfig struct { + // Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` + Hostname *string `json:"hostname,omitempty"` + // Reference to an existing `FeatureStore` CR in the same k8s cluster. + FeastRef *FeatureStoreRef `json:"feastRef,omitempty"` +} + +// FeatureStoreRef defines which existing FeatureStore's registry should be used +type FeatureStoreRef struct { + // Name of the FeatureStore + Name string `json:"name"` + // Namespace of the FeatureStore + Namespace string `json:"namespace,omitempty"` +} + +// ServiceConfigs k8s container settings +type ServiceConfigs struct { + DefaultConfigs `json:",inline"` + OptionalConfigs `json:",inline"` +} + +// DefaultConfigs k8s container settings that are applied by default +type DefaultConfigs struct { + Image *string `json:"image,omitempty"` +} + +// OptionalConfigs k8s container settings that are optional +type OptionalConfigs struct { + ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` } // FeatureStoreStatus defines the observed state of FeatureStore type FeatureStoreStatus struct { - Applied FeatureStoreSpec `json:"applied,omitempty"` + // Shows the currently applied feast configuration, including any pertinent defaults + Applied FeatureStoreSpec `json:"applied,omitempty"` + // ConfigMap in this namespace containing a client `feature_store.yaml` for this feast deployment ClientConfigMap string `json:"clientConfigMap,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` - FeastVersion string `json:"feastVersion,omitempty"` - Phase string `json:"phase,omitempty"` - ServiceUrls ServiceUrls `json:"serviceUrls,omitempty"` + // Version of feast that's currently deployed + FeastVersion string `json:"feastVersion,omitempty"` + Phase string `json:"phase,omitempty"` + ServiceHostnames ServiceHostnames `json:"serviceHostnames,omitempty"` } -// ServiceUrls -type ServiceUrls struct { - Registry string `json:"registry,omitempty"` +// 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"` } //+kubebuilder:object:root=true diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index b8e410a616e..3df696dad0c 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -21,16 +21,37 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultConfigs) DeepCopyInto(out *DefaultConfigs) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultConfigs. +func (in *DefaultConfigs) DeepCopy() *DefaultConfigs { + if in == nil { + return nil + } + out := new(DefaultConfigs) + 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 out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -84,9 +105,59 @@ func (in *FeatureStoreList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreRef) DeepCopyInto(out *FeatureStoreRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreRef. +func (in *FeatureStoreRef) DeepCopy() *FeatureStoreRef { + if in == nil { + return nil + } + out := new(FeatureStoreRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { + *out = *in + if in.OfflineStore != nil { + in, out := &in.OfflineStore, &out.OfflineStore + *out = new(OfflineStore) + (*in).DeepCopyInto(*out) + } + if in.OnlineStore != nil { + in, out := &in.OnlineStore, &out.OnlineStore + *out = new(OnlineStore) + (*in).DeepCopyInto(*out) + } + if in.Registry != nil { + in, out := &in.Registry, &out.Registry + *out = new(Registry) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreServices. +func (in *FeatureStoreServices) DeepCopy() *FeatureStoreServices { + if in == nil { + return nil + } + out := new(FeatureStoreServices) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(FeatureStoreServices) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. @@ -102,15 +173,15 @@ func (in *FeatureStoreSpec) DeepCopy() *FeatureStoreSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeatureStoreStatus) DeepCopyInto(out *FeatureStoreStatus) { *out = *in - out.Applied = in.Applied + in.Applied.DeepCopyInto(&out.Applied) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.ServiceUrls = in.ServiceUrls + out.ServiceHostnames = in.ServiceHostnames } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreStatus. @@ -124,16 +195,156 @@ func (in *FeatureStoreStatus) DeepCopy() *FeatureStoreStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceUrls) DeepCopyInto(out *ServiceUrls) { +func (in *LocalRegistryConfig) DeepCopyInto(out *LocalRegistryConfig) { + *out = *in + in.ServiceConfigs.DeepCopyInto(&out.ServiceConfigs) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRegistryConfig. +func (in *LocalRegistryConfig) DeepCopy() *LocalRegistryConfig { + if in == nil { + return nil + } + out := new(LocalRegistryConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStore) DeepCopyInto(out *OfflineStore) { + *out = *in + in.ServiceConfigs.DeepCopyInto(&out.ServiceConfigs) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStore. +func (in *OfflineStore) DeepCopy() *OfflineStore { + if in == nil { + return nil + } + out := new(OfflineStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { + *out = *in + in.ServiceConfigs.DeepCopyInto(&out.ServiceConfigs) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStore. +func (in *OnlineStore) DeepCopy() *OnlineStore { + if in == nil { + return nil + } + out := new(OnlineStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptionalConfigs) DeepCopyInto(out *OptionalConfigs) { + *out = *in + if in.ImagePullPolicy != nil { + in, out := &in.ImagePullPolicy, &out.ImagePullPolicy + *out = new(v1.PullPolicy) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionalConfigs. +func (in *OptionalConfigs) DeepCopy() *OptionalConfigs { + if in == nil { + return nil + } + out := new(OptionalConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Registry) DeepCopyInto(out *Registry) { + *out = *in + if in.Local != nil { + in, out := &in.Local, &out.Local + *out = new(LocalRegistryConfig) + (*in).DeepCopyInto(*out) + } + if in.Remote != nil { + in, out := &in.Remote, &out.Remote + *out = new(RemoteRegistryConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. +func (in *Registry) DeepCopy() *Registry { + if in == nil { + return nil + } + out := new(Registry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteRegistryConfig) DeepCopyInto(out *RemoteRegistryConfig) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.FeastRef != nil { + in, out := &in.FeastRef, &out.FeastRef + *out = new(FeatureStoreRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteRegistryConfig. +func (in *RemoteRegistryConfig) DeepCopy() *RemoteRegistryConfig { + if in == nil { + return nil + } + out := new(RemoteRegistryConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceConfigs) DeepCopyInto(out *ServiceConfigs) { + *out = *in + in.DefaultConfigs.DeepCopyInto(&out.DefaultConfigs) + in.OptionalConfigs.DeepCopyInto(&out.OptionalConfigs) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceConfigs. +func (in *ServiceConfigs) DeepCopy() *ServiceConfigs { + if in == nil { + return nil + } + out := new(ServiceConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceHostnames) DeepCopyInto(out *ServiceHostnames) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceUrls. -func (in *ServiceUrls) DeepCopy() *ServiceUrls { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceHostnames. +func (in *ServiceHostnames) DeepCopy() *ServiceHostnames { if in == nil { return nil } - out := new(ServiceUrls) + out := new(ServiceHostnames) in.DeepCopyInto(out) return out } diff --git a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml index 2a272660ec2..245db443581 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml @@ -16,7 +16,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2024-10-28T15:51:17Z" + createdAt: "2024-11-01T13:05:11Z" operators.operatorframework.io/builder: operator-sdk-v1.37.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: feast-operator.v0.41.0 diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml index 2fb15b432a7..abefdd89bac 100644 --- a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -54,6 +54,248 @@ spec: underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed registry + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object @@ -61,7 +303,8 @@ spec: description: FeatureStoreStatus defines the observed state of FeatureStore properties: applied: - description: FeatureStoreSpec defines the desired state of FeatureStore + description: Shows the currently applied feast configuration, including + any pertinent defaults properties: feastProject: description: FeastProject is the Feast project id. This can be @@ -69,10 +312,257 @@ spec: with an underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline + store service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed + registry service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment type: string conditions: items: @@ -144,12 +634,18 @@ spec: type: object type: array feastVersion: + description: Version of feast that's currently deployed type: string phase: type: string - serviceUrls: - description: ServiceUrls + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 properties: + offlineStore: + type: string + onlineStore: + type: string registry: type: string type: object diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go index eae4f8b1214..e132a6a3c9c 100644 --- a/infra/feast-operator/cmd/main.go +++ b/infra/feast-operator/cmd/main.go @@ -122,6 +122,7 @@ func main() { Cache: &client.CacheOptions{ DisableFor: []client.Object{ &corev1.ConfigMap{}, + &corev1.Secret{}, }, }, }, 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 8c5c2e62a6d..611190f85aa 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -54,6 +54,248 @@ spec: underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed registry + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object @@ -61,7 +303,8 @@ spec: description: FeatureStoreStatus defines the observed state of FeatureStore properties: applied: - description: FeatureStoreSpec defines the desired state of FeatureStore + description: Shows the currently applied feast configuration, including + any pertinent defaults properties: feastProject: description: FeastProject is the Feast project id. This can be @@ -69,10 +312,257 @@ spec: with an underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline + store service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed + registry service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment type: string conditions: items: @@ -144,12 +634,18 @@ spec: type: object type: array feastVersion: + description: Version of feast that's currently deployed type: string phase: type: string - serviceUrls: - description: ServiceUrls + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 properties: + offlineStore: + type: string + onlineStore: + type: string registry: type: string type: object diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 77de70ebcb8..f20b49d3095 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -62,6 +62,248 @@ spec: underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed registry + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object @@ -69,7 +311,8 @@ spec: description: FeatureStoreStatus defines the observed state of FeatureStore properties: applied: - description: FeatureStoreSpec defines the desired state of FeatureStore + description: Shows the currently applied feast configuration, including + any pertinent defaults properties: feastProject: description: FeastProject is the Feast project id. This can be @@ -77,10 +320,257 @@ spec: with an underscore. Required. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ type: string + services: + description: FeatureStoreServices defines the desired feast service + deployments. ephemeral registry is deployed by default. + properties: + offlineStore: + description: OfflineStore configures the deployed offline + store service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the deployed online store + service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 deployed + registry service + properties: + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + 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. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + 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. It makes that resource available + inside a container. + 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.io/docs/concepts/configuration/manage-resources-containers/ + 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. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + remote: + description: |- + RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. + Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. + 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 + 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)' + type: object required: - feastProject type: object clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment type: string conditions: items: @@ -152,12 +642,18 @@ spec: type: object type: array feastVersion: + description: Version of feast that's currently deployed type: string phase: type: string - serviceUrls: - description: ServiceUrls + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 properties: + offlineStore: + type: string + onlineStore: + type: string registry: type: string type: object diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index 17227293d80..1502bf38945 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -23,14 +23,16 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" @@ -75,11 +77,12 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request } currentStatus := cr.Status.DeepCopy() + // initial status defaults must occur before feast deployment applyDefaultsToStatus(cr) result, recErr = r.deployFeast(ctx, cr) if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) { if err := r.Client.Status().Update(ctx, cr); err != nil { - if errors.IsConflict(err) { + if apierrors.IsConflict(err) { logger.Info("FeatureStore object modified, retry syncing status") // Re-queue and preserve existing recErr result = ctrl.Result{Requeue: true, RequeueAfter: RequeueDelayError} @@ -103,25 +106,25 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Reason: feastdevv1alpha1.ReadyReason, Message: feastdevv1alpha1.ReadyMessage, } + feast := services.FeastServices{ Client: r.Client, Context: ctx, FeatureStore: cr, Scheme: r.Scheme, } - err = feast.Deploy() - if err != nil { + if err = feast.Deploy(); err != nil { condition = metav1.Condition{ Type: feastdevv1alpha1.ReadyType, Status: metav1.ConditionFalse, Reason: feastdevv1alpha1.FailedReason, Message: "Error: " + err.Error(), } - result = ctrl.Result{Requeue: true} + result = ctrl.Result{Requeue: true, RequeueAfter: RequeueDelayError} } + logger.Info(condition.Message) apimeta.SetStatusCondition(&cr.Status.Conditions, condition) - if apimeta.IsStatusConditionTrue(cr.Status.Conditions, feastdevv1alpha1.ReadyType) { cr.Status.Phase = feastdevv1alpha1.ReadyPhase } else if apimeta.IsStatusConditionFalse(cr.Status.Conditions, feastdevv1alpha1.ReadyType) { @@ -140,10 +143,79 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.ConfigMap{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). + Watches(&feastdevv1alpha1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)). Complete(r) } +// if a remotely referenced FeatureStore is changed, reconcile any FeatureStores that reference it. +func (r *FeatureStoreReconciler) mapFeastRefsToFeastRequests(ctx context.Context, object client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + feastRef := object.(*feastdevv1alpha1.FeatureStore) + + // list all FeatureStores in the cluster + var feastList feastdevv1alpha1.FeatureStoreList + if err := r.List(ctx, &feastList, client.InNamespace("")); err != nil { + logger.Error(err, "could not list FeatureStores. "+ + "FeatureStores affected by changes to the referenced FeatureStore object will not be reconciled.") + return nil + } + + feastRefNsName := client.ObjectKeyFromObject(feastRef) + var requests []reconcile.Request + for _, obj := range feastList.Items { + objNsName := client.ObjectKeyFromObject(&obj) + if feastRefNsName != objNsName { + feast := services.FeastServices{ + Client: r.Client, + Context: ctx, + FeatureStore: &obj, + Scheme: r.Scheme, + } + if feast.IsRemoteRefRegistry() { + remoteRef := obj.Status.Applied.Services.Registry.Remote.FeastRef + remoteRefNsName := types.NamespacedName{Name: remoteRef.Name, Namespace: remoteRef.Namespace} + if feastRefNsName == remoteRefNsName { + requests = append(requests, reconcile.Request{NamespacedName: objNsName}) + } + } + } + } + + return requests +} + func applyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { - cr.Status.Applied.FeastProject = cr.Spec.FeastProject cr.Status.FeastVersion = feastversion.FeastVersion + applied := cr.Spec.DeepCopy() + if applied.Services == nil { + applied.Services = &feastdevv1alpha1.FeatureStoreServices{} + } + + // default to registry service deployment + if applied.Services.Registry == nil { + applied.Services.Registry = &feastdevv1alpha1.Registry{} + } + // if remote registry not set, proceed w/ local registry defaults + if applied.Services.Registry.Remote == nil { + // if local registry not set, apply an empty pointer struct + if applied.Services.Registry.Local == nil { + applied.Services.Registry.Local = &feastdevv1alpha1.LocalRegistryConfig{} + } + setServiceDefaultConfigs(&applied.Services.Registry.Local.ServiceConfigs.DefaultConfigs) + } + if applied.Services.OfflineStore != nil { + setServiceDefaultConfigs(&applied.Services.OfflineStore.ServiceConfigs.DefaultConfigs) + } + if applied.Services.OnlineStore != nil { + setServiceDefaultConfigs(&applied.Services.OnlineStore.ServiceConfigs.DefaultConfigs) + } + + // overwrite status.applied with every reconcile + applied.DeepCopyInto(&cr.Status.Applied) +} + +func setServiceDefaultConfigs(defaultConfigs *feastdevv1alpha1.DefaultConfigs) { + if defaultConfigs.Image == nil { + defaultConfigs.Image = &services.DefaultImage + } } diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 5b1e41bb309..c7b04d0b26a 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -28,8 +28,11 @@ import ( "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -76,7 +79,7 @@ var _ = Describe("FeatureStore Controller", func() { }) It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") + By("Reconciling the minimal created resource") controllerReconciler := &FeatureStoreReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), @@ -91,6 +94,25 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(1)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(1)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + feast := services.FeastServices{ Client: controllerReconciler.Client, Context: ctx, @@ -100,10 +122,21 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status).NotTo(BeNil()) Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) - Expect(resource.Status.ServiceUrls.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + ".svc.cluster.local:80")) + Expect(resource.Status.ServiceHostnames.OfflineStore).To(BeEmpty()) + Expect(resource.Status.ServiceHostnames.OnlineStore).To(BeEmpty()) + Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + ".svc.cluster.local:80")) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Conditions).NotTo(BeEmpty()) + Expect(resource.Status.Applied.Services).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Remote).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.ImagePullPolicy).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Resources).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Image).To(Equal(&services.DefaultImage)) + Expect(resource.Status.Conditions).NotTo(BeEmpty()) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -134,6 +167,7 @@ var _ = Describe("FeatureStore Controller", func() { }, deploy) Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) @@ -145,8 +179,7 @@ var _ = Describe("FeatureStore Controller", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.RegistryPort)))) - + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) }) It("should properly encode a feature_store.yaml config", func() { @@ -181,10 +214,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) - env := getEnvVar(services.FeatureStoreYamlEnvVar, deploy.Spec.Template.Spec.Containers[0].Env) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) Expect(env).NotTo(BeNil()) - fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64() + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) Expect(fsYamlStr).To(Equal(env.Value)) @@ -204,6 +237,29 @@ var _ = Describe("FeatureStore Controller", func() { } Expect(repoConfig).To(Equal(testConfig)) + // check client config + cm := &corev1.ConfigMap{} + name := feast.GetFeastServiceName(services.ClientFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: resource.Namespace, + }, + cm) + Expect(err).NotTo(HaveOccurred()) + repoConfigClient := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(cm.Data[services.FeatureStoreYamlCmKey]), repoConfigClient) + Expect(err).NotTo(HaveOccurred()) + clientConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: "feast-test-resource-registry.default.svc.cluster.local:80", + }, + } + Expect(repoConfigClient).To(Equal(clientConfig)) + // change feast project and reconcile resourceNew := resource.DeepCopy() resourceNew.Spec.FeastProject = "changed" @@ -226,10 +282,10 @@ var _ = Describe("FeatureStore Controller", func() { testConfig.Project = resourceNew.Spec.FeastProject Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) - env = getEnvVar(services.FeatureStoreYamlEnvVar, deploy.Spec.Template.Spec.Containers[0].Env) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) Expect(env).NotTo(BeNil()) - fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64() + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) Expect(err).NotTo(HaveOccurred()) Expect(fsYamlStr).To(Equal(env.Value)) @@ -322,11 +378,753 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.FailedPhase)) }) }) + + Context("When reconciling a resource with all services enabled", func() { + const resourceName = "services" + image := "test:latest" + var pullPolicy corev1.PullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + featurestore := &feastdevv1alpha1.FeatureStore{} + + BeforeEach(func() { + By("creating the custom resource for the Kind FeatureStore") + err := k8sClient.Get(ctx, typeNamespacedName, featurestore) + if err != nil && errors.IsNotFound(err) { + resource := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{}, + OnlineStore: &feastdevv1alpha1.OnlineStore{ + ServiceConfigs: feastdevv1alpha1.ServiceConfigs{ + DefaultConfigs: feastdevv1alpha1.DefaultConfigs{ + Image: &image, + }, + OptionalConfigs: feastdevv1alpha1.OptionalConfigs{ + ImagePullPolicy: &pullPolicy, + Resources: &corev1.ResourceRequirements{}, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + } + Expect(resource.Status).NotTo(BeNil()) + Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) + Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) + Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) + Expect(resource.Status.Applied.Services).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.ImagePullPolicy).To(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Resources).To(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Image).To(Equal(&services.DefaultImage)) + Expect(resource.Status.Applied.Services.OnlineStore).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.ImagePullPolicy).To(Equal(&pullPolicy)) + Expect(resource.Status.Applied.Services.OnlineStore.Resources).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Image).To(Equal(&image)) + Expect(resource.Status.Applied.Services.Registry).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.ImagePullPolicy).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Resources).To(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Image).To(Equal(&services.DefaultImage)) + + domain := ".svc.cluster.local:80" + Expect(resource.Status.ServiceHostnames.OfflineStore).To(Equal(feast.GetFeastServiceName(services.OfflineFeastType) + "." + resource.Namespace + domain)) + Expect(resource.Status.ServiceHostnames.OnlineStore).To(Equal(feast.GetFeastServiceName(services.OnlineFeastType) + "." + resource.Namespace + domain)) + Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) + + Expect(resource.Status.Conditions).NotTo(BeEmpty()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + 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), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + }) + + It("should properly encode a feature_store.yaml config", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + + feast := services.FeastServices{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + } + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + RegistryType: services.RegistryFileConfigType, + Path: services.LocalRegistryPath, + }, + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: "feast-services-registry.default.svc.cluster.local:80", + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineDaskConfigType, + }, + Registry: regRemote, + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: "feast-services-offline.default.svc.cluster.local", + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Path: services.LocalOnlinePath, + Type: services.OnlineSqliteConfigType, + }, + Registry: regRemote, + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + + // check client config + cm := &corev1.ConfigMap{} + name := feast.GetFeastServiceName(services.ClientFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: resource.Namespace, + }, + cm) + Expect(err).NotTo(HaveOccurred()) + repoConfigClient := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(cm.Data[services.FeatureStoreYamlCmKey]), repoConfigClient) + Expect(err).NotTo(HaveOccurred()) + clientConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Path: "http://feast-services-online.default.svc.cluster.local:80", + Type: services.OnlineRemoteConfigType, + }, + Registry: regRemote, + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change feast project and reconcile + resourceNew := resource.DeepCopy() + resourceNew.Spec.FeastProject = "changed" + err = k8sClient.Update(ctx, resourceNew) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + 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: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + + 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(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig).To(Equal(testConfig)) + }) + + It("Should delete k8s objects owned by the FeatureStore CR", func() { + By("changing which feast services are configured in the CR") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + // disable the Online Store service + resource.Spec.Services.OnlineStore = nil + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(2)) + + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(2)) + + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + // disable the Offline Store service as well + resource.Spec.Services.OfflineStore = nil + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(1)) + + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(1)) + }) + + It("should handle remote registry references", func() { + By("By properly configuring feast") + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + referencedRegistry := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, referencedRegistry) + Expect(err).NotTo(HaveOccurred()) + + name := "remote-registry-reference" + resource := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: referencedRegistry.Namespace, + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: referencedRegistry.Spec.FeastProject, + Services: &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{}, + OfflineStore: &feastdevv1alpha1.OfflineStore{}, + Registry: &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + Name: name, + }, + }, + }, + }, + }, + } + resource.SetGroupVersionKind(feastdevv1alpha1.GroupVersion.WithKind("FeatureStore")) + nsName := client.ObjectKeyFromObject(resource) + err = k8sClient.Create(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).To(HaveOccurred()) + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).NotTo(BeNil()) + Expect(cond.Message).To(Equal("Error: FeatureStore '" + name + "' can't reference itself in `spec.services.registry.remote.feastRef`")) + + resource.Spec.Services.Registry.Remote.FeastRef.Name = "wrong" + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).To(HaveOccurred()) + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).NotTo(BeNil()) + Expect(cond.Message).To(Equal("Error: Referenced FeatureStore '" + resource.Spec.Services.Registry.Remote.FeastRef.Name + "' was not found")) + + resource.Spec.Services.Registry.Remote.FeastRef.Name = referencedRegistry.Name + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType)).To(BeTrue()) + Expect(resource.Status.Applied.Services.Registry.Remote.FeastRef.Namespace).To(Equal(resource.Namespace)) + Expect(resource.Status.ServiceHostnames.Registry).ToNot(BeEmpty()) + Expect(resource.Status.ServiceHostnames.Registry).To(Equal(referencedRegistry.Status.ServiceHostnames.Registry)) + feast := services.FeastServices{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + } + + // check client config + cm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.ClientFeastType), + Namespace: resource.Namespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + repoConfigClient := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(cm.Data[services.FeatureStoreYamlCmKey]), repoConfigClient) + Expect(err).NotTo(HaveOccurred()) + clientConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Host: "feast-" + resource.Name + "-offline.default.svc.cluster.local", + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + }, + OnlineStore: services.OnlineStoreConfig{ + Path: "http://feast-" + resource.Name + "-online.default.svc.cluster.local:80", + Type: services.OnlineRemoteConfigType, + }, + Registry: services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: "feast-" + referencedRegistry.Name + "-registry.default.svc.cluster.local:80", + }, + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + hostname := "test:80" + referencedRegistry.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + Hostname: &hostname, + }, + } + err = k8sClient.Update(ctx, referencedRegistry) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).To(HaveOccurred()) + + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(resource.Status.ServiceHostnames.Registry).To(BeEmpty()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType)).To(BeTrue()) + Expect(resource.Status.Applied.Services.Registry.Remote.FeastRef.Name).To(Equal(referencedRegistry.Name)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).NotTo(BeNil()) + Expect(cond.Message).To(Equal("Error: Remote feast registry of referenced FeatureStore '" + referencedRegistry.Name + "' is not ready")) + }) + + It("should error on reconcile", func() { + By("Trying to set the controller OwnerRef of a Deployment that already has a controller") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + } + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + + err = controllerutil.RemoveControllerReference(resource, deploy, controllerReconciler.Scheme) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(deploy)).To(BeFalse()) + + svc := &corev1.Service{} + name := feast.GetFeastServiceName(services.OfflineFeastType) + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + err = controllerutil.SetControllerReference(svc, deploy, controllerReconciler.Scheme) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + err = k8sClient.Update(ctx, deploy) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(resource.Status.Conditions).To(HaveLen(5)) + + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.FailedReason)) + Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + name + " is already owned by another Service controller " + name)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.OfflineStoreFailedReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + name + " is already owned by another Service controller " + name)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.FailedPhase)) + }) + + It("should error on reconcile", func() { + By("By failing to pass CRD schema validation") + + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + Expect(resource.Spec.Services.Registry).To(BeNil()) + + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{} + err = k8sClient.Update(ctx, resource) + Expect(err).To(HaveOccurred()) + + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{}, + Remote: &feastdevv1alpha1.RemoteRegistryConfig{}, + } + err = k8sClient.Update(ctx, resource) + Expect(err).To(HaveOccurred()) + + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{}, + } + err = k8sClient.Update(ctx, resource) + Expect(err).To(HaveOccurred()) + + hostname := "test:80" + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + Hostname: &hostname, + FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + Name: "test", + }, + }, + } + err = k8sClient.Update(ctx, resource) + Expect(err).To(HaveOccurred()) + + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + Name: "test", + }, + }, + } + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + }) + }) }) -func getEnvVar(name string, envs []corev1.EnvVar) *corev1.EnvVar { +func getFeatureStoreYamlEnvVar(envs []corev1.EnvVar) *corev1.EnvVar { for _, e := range envs { - if e.Name == name { + if e.Name == services.FeatureStoreYamlEnvVar { return &e } } diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index a46a7c63390..1befd2df194 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -17,8 +17,6 @@ limitations under the License. package services import ( - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" - "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" @@ -26,9 +24,9 @@ import ( func (feast *FeastServices) deployClient() error { if err := feast.createClientConfigMap(); err != nil { - return err + return feast.setFeastServiceCondition(err, ClientFeastType) } - return nil + return feast.setFeastServiceCondition(nil, ClientFeastType) } func (feast *FeastServices) createClientConfigMap() error { @@ -53,27 +51,7 @@ func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { if err != nil { return err } - cm.Data = map[string]string{"feature_store.yaml": string(clientYaml)} + cm.Data = map[string]string{FeatureStoreYamlCmKey: string(clientYaml)} feast.FeatureStore.Status.ClientConfigMap = cm.Name return controllerutil.SetControllerReference(feast.FeatureStore, cm, feast.Scheme) } - -func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { - return yaml.Marshal(feast.getClientRepoConfig()) -} - -func (feast *FeastServices) getClientRepoConfig() RepoConfig { - status := feast.FeatureStore.Status - clientRepoConfig := RepoConfig{ - Project: status.Applied.FeastProject, - Provider: LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, - } - if len(status.ServiceUrls.Registry) > 0 { - clientRepoConfig.Registry = RegistryConfig{ - RegistryType: RegistryRemoteConfigType, - Path: status.ServiceUrls.Registry, - } - } - return clientRepoConfig -} diff --git a/infra/feast-operator/internal/controller/services/registry.go b/infra/feast-operator/internal/controller/services/registry.go deleted file mode 100644 index 76e01e67982..00000000000 --- a/infra/feast-operator/internal/controller/services/registry.go +++ /dev/null @@ -1,245 +0,0 @@ -/* -Copyright 2024 Feast Community. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package services - -import ( - "encoding/base64" - - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" - "gopkg.in/yaml.v3" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// Deploy the feast services -func (feast *FeastServices) Deploy() error { - logger := log.FromContext(feast.Context) - cr := feast.FeatureStore - - if err := feast.deployRegistry(); err != nil { - apimeta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{ - Type: feastdevv1alpha1.RegistryReadyType, - Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.RegistryFailedReason, - Message: "Error: " + err.Error(), - }) - logger.Error(err, "Error deploying the FeatureStore "+string(RegistryFeastType)+" service") - return err - } else { - apimeta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{ - Type: feastdevv1alpha1.RegistryReadyType, - Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.RegistryReadyMessage, - }) - } - - if err := feast.deployClient(); err != nil { - apimeta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{ - Type: feastdevv1alpha1.ClientReadyType, - Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.ClientFailedReason, - Message: "Error: " + err.Error(), - }) - logger.Error(err, "Error deploying the FeatureStore "+string(ClientFeastType)+" service") - return err - } else { - apimeta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{ - Type: feastdevv1alpha1.ClientReadyType, - Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.ClientReadyMessage, - }) - } - return nil -} - -func (feast *FeastServices) deployRegistry() error { - if err := feast.createRegistryDeployment(); err != nil { - return err - } - if err := feast.createRegistryService(); err != nil { - return err - } - return nil -} - -func (feast *FeastServices) createRegistryDeployment() error { - logger := log.FromContext(feast.Context) - deploy := &appsv1.Deployment{ - ObjectMeta: feast.GetObjectMeta(RegistryFeastType), - } - deploy.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, deploy, controllerutil.MutateFn(func() error { - return feast.setDeployment(deploy, RegistryFeastType) - })); err != nil { - return err - } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { - logger.Info("Successfully reconciled", "Deployment", deploy.Name, "operation", op) - } - - return nil -} - -func (feast *FeastServices) createRegistryService() error { - logger := log.FromContext(feast.Context) - svc := &corev1.Service{ - ObjectMeta: feast.GetObjectMeta(RegistryFeastType), - } - svc.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, svc, controllerutil.MutateFn(func() error { - return feast.setService(svc, RegistryFeastType) - })); err != nil { - return err - } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { - logger.Info("Successfully reconciled", "Service", svc.Name, "operation", op) - } - return nil -} - -func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType FeastServiceType) error { - fsYamlB64, err := feast.GetServiceFeatureStoreYamlBase64() - if err != nil { - return err - } - replicas := int32(1) - deploy.Labels = feast.getLabels(feastType) - deploy.Spec = appsv1.DeploymentSpec{ - Replicas: &replicas, - Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: deploy.GetLabels(), - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: string(feastType), - Image: "feastdev/feature-server:" + feast.FeatureStore.Status.FeastVersion, - ImagePullPolicy: corev1.PullIfNotPresent, - Env: []corev1.EnvVar{ - { - Name: FeatureStoreYamlEnvVar, - Value: fsYamlB64, - }, - }, - }, - }, - }, - }, - } - if feastType == RegistryFeastType { - deploy.Spec.Template.Spec.Containers[0].Command = []string{ - "feast", "serve_registry", - } - deploy.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{ - { - Name: string(feastType), - ContainerPort: RegistryPort, - Protocol: corev1.ProtocolTCP, - }, - } - probeHandler := corev1.ProbeHandler{ - TCPSocket: &corev1.TCPSocketAction{ - Port: intstr.FromInt(int(RegistryPort)), - }, - } - deploy.Spec.Template.Spec.Containers[0].LivenessProbe = &corev1.Probe{ - ProbeHandler: probeHandler, - InitialDelaySeconds: 30, - PeriodSeconds: 30, - } - deploy.Spec.Template.Spec.Containers[0].ReadinessProbe = &corev1.Probe{ - ProbeHandler: probeHandler, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - } - } - return controllerutil.SetControllerReference(feast.FeatureStore, deploy, feast.Scheme) -} - -func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType) error { - svc.Labels = feast.getLabels(feastType) - svc.Spec = corev1.ServiceSpec{ - Selector: svc.GetLabels(), - Type: corev1.ServiceTypeClusterIP, - } - if feastType == RegistryFeastType { - svc.Spec.Ports = []corev1.ServicePort{ - { - Name: "http", - Port: int32(80), - Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromInt(int(RegistryPort)), - }, - } - feast.FeatureStore.Status.ServiceUrls.Registry = svc.Name + "." + svc.Namespace + ".svc.cluster.local:80" - } - return controllerutil.SetControllerReference(feast.FeatureStore, svc, feast.Scheme) -} - -// GetObjectMeta returns the feast k8s object metadata -func (feast *FeastServices) GetObjectMeta(feastType FeastServiceType) metav1.ObjectMeta { - return metav1.ObjectMeta{Name: feast.GetFeastServiceName(feastType), Namespace: feast.FeatureStore.Namespace} -} - -func (feast *FeastServices) getLabels(feastType FeastServiceType) map[string]string { - return map[string]string{ - feastdevv1alpha1.GroupVersion.Group + "/name": feast.FeatureStore.Name, - feastdevv1alpha1.GroupVersion.Group + "/service-type": string(feastType), - } -} - -func (feast *FeastServices) getFeastName() string { - return FeastPrefix + feast.FeatureStore.Name -} - -// GetFeastServiceName returns the feast service object name based on service type -func (feast *FeastServices) GetFeastServiceName(feastType FeastServiceType) string { - return feast.getFeastName() + "-" + string(feastType) -} - -// GetServiceFeatureStoreYamlBase64 returns a base64 encoded feature_store.yaml config for the feast service -func (feast *FeastServices) GetServiceFeatureStoreYamlBase64() (string, error) { - fsYaml, err := feast.getServiceFeatureStoreYaml() - if err != nil { - return "", err - } - return base64.StdEncoding.EncodeToString(fsYaml), nil -} - -func (feast *FeastServices) getServiceFeatureStoreYaml() ([]byte, error) { - return yaml.Marshal(feast.getServiceRepoConfig()) -} - -func (feast *FeastServices) getServiceRepoConfig() RepoConfig { - appliedSpec := feast.FeatureStore.Status.Applied - return RepoConfig{ - Project: appliedSpec.FeastProject, - Provider: LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, - Registry: RegistryConfig{ - RegistryType: RegistryFileConfigType, - Path: LocalRegistryPath, - }, - } -} diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go new file mode 100644 index 00000000000..3137417f3ac --- /dev/null +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -0,0 +1,105 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "encoding/base64" + "strings" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" +) + +// GetServiceFeatureStoreYamlBase64 returns a base64 encoded feature_store.yaml config for the feast service +func (feast *FeastServices) GetServiceFeatureStoreYamlBase64(feastType FeastServiceType) (string, error) { + fsYaml, err := feast.getServiceFeatureStoreYaml(feastType) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(fsYaml), nil +} + +func (feast *FeastServices) getServiceFeatureStoreYaml(feastType FeastServiceType) ([]byte, error) { + return yaml.Marshal(feast.getServiceRepoConfig(feastType)) +} + +func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) RepoConfig { + appliedSpec := feast.FeatureStore.Status.Applied + + repoConfig := feast.getClientRepoConfig() + if appliedSpec.Services != nil { + // Offline server has an `offline_store` section and a remote `registry` + if feastType == OfflineFeastType && appliedSpec.Services.OfflineStore != nil { + repoConfig.OfflineStore = OfflineStoreConfig{ + Type: OfflineDaskConfigType, + } + repoConfig.OnlineStore = OnlineStoreConfig{} + } + // Online server has an `online_store` section, a remote `registry` and a remote `offline_store` + if feastType == OnlineFeastType && appliedSpec.Services.OnlineStore != nil { + repoConfig.OnlineStore = OnlineStoreConfig{ + Type: OnlineSqliteConfigType, + Path: LocalOnlinePath, + } + } + // Registry server only has a `registry` section + if feastType == RegistryFeastType && feast.isLocalRegistry() { + repoConfig.Registry = RegistryConfig{ + RegistryType: RegistryFileConfigType, + Path: LocalRegistryPath, + } + repoConfig.OfflineStore = OfflineStoreConfig{} + repoConfig.OnlineStore = OnlineStoreConfig{} + } + } + + return repoConfig +} + +func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { + return yaml.Marshal(feast.getClientRepoConfig()) +} + +func (feast *FeastServices) getClientRepoConfig() RepoConfig { + status := feast.FeatureStore.Status + clientRepoConfig := RepoConfig{ + Project: status.Applied.FeastProject, + Provider: LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + } + if len(status.ServiceHostnames.OfflineStore) > 0 { + clientRepoConfig.OfflineStore = OfflineStoreConfig{ + Type: OfflineRemoteConfigType, + Host: strings.Split(status.ServiceHostnames.OfflineStore, ":")[0], + Port: HttpPort, + } + } + if len(status.ServiceHostnames.OnlineStore) > 0 { + clientRepoConfig.OnlineStore = OnlineStoreConfig{ + Type: OnlineRemoteConfigType, + Path: strings.ToLower(string(corev1.URISchemeHTTP)) + "://" + status.ServiceHostnames.OnlineStore, + } + } + if len(status.ServiceHostnames.Registry) > 0 { + clientRepoConfig.Registry = RegistryConfig{ + RegistryType: RegistryRemoteConfigType, + Path: status.ServiceHostnames.Registry, + } + } + return clientRepoConfig +} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go new file mode 100644 index 00000000000..821f81f4e94 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/services.go @@ -0,0 +1,398 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "errors" + "strconv" + "strings" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Deploy the feast services +func (feast *FeastServices) Deploy() error { + if err := feast.setServiceHostnames(); err != nil { + return err + } + + services := feast.FeatureStore.Status.Applied.Services + if services != nil { + if services.OfflineStore != nil { + if err := feast.deployFeastServiceByType(OfflineFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(OfflineFeastType); err != nil { + return err + } + } + + if services.OnlineStore != nil { + if err := feast.deployFeastServiceByType(OnlineFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(OnlineFeastType); err != nil { + return err + } + } + + if feast.isLocalRegistry() { + if err := feast.deployFeastServiceByType(RegistryFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(RegistryFeastType); err != nil { + return err + } + } + } + + if err := feast.deployClient(); err != nil { + return err + } + + return nil +} + +func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) error { + if err := feast.createService(feastType); err != nil { + return feast.setFeastServiceCondition(err, feastType) + } + if err := feast.createDeployment(feastType); err != nil { + return feast.setFeastServiceCondition(err, feastType) + } + return feast.setFeastServiceCondition(nil, feastType) +} + +func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) error { + if err := feast.deleteOwnedFeastObj(feast.initFeastDeploy(feastType)); err != nil { + return err + } + if err := feast.deleteOwnedFeastObj(feast.initFeastSvc(feastType)); err != nil { + return err + } + apimeta.RemoveStatusCondition(&feast.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) + return nil +} + +func (feast *FeastServices) createService(feastType FeastServiceType) error { + logger := log.FromContext(feast.Context) + svc := feast.initFeastSvc(feastType) + if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, svc, controllerutil.MutateFn(func() error { + return feast.setService(svc, feastType) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Service", svc.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) createDeployment(feastType FeastServiceType) error { + logger := log.FromContext(feast.Context) + deploy := feast.initFeastDeploy(feastType) + if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, deploy, controllerutil.MutateFn(func() error { + return feast.setDeployment(deploy, feastType) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Deployment", deploy.Name, "operation", op) + } + + return nil +} + +func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType FeastServiceType) error { + fsYamlB64, err := feast.GetServiceFeatureStoreYamlBase64(feastType) + if err != nil { + return err + } + deploy.Labels = feast.getLabels(feastType) + deploySettings := FeastServiceConstants[feastType] + serviceConfigs := feast.getServiceConfigs(feastType) + defaultServiceConfigs := serviceConfigs.DefaultConfigs + + // standard configs are applied here + probeHandler := corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(int(deploySettings.TargetPort)), + }, + } + deploy.Spec = appsv1.DeploymentSpec{ + Replicas: &DefaultReplicas, + Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: deploy.GetLabels(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: string(feastType), + Image: *defaultServiceConfigs.Image, + Command: deploySettings.Command, + Ports: []corev1.ContainerPort{ + { + Name: string(feastType), + ContainerPort: deploySettings.TargetPort, + Protocol: corev1.ProtocolTCP, + }, + }, + Env: []corev1.EnvVar{ + { + Name: FeatureStoreYamlEnvVar, + Value: fsYamlB64, + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + InitialDelaySeconds: 30, + PeriodSeconds: 30, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + InitialDelaySeconds: 20, + PeriodSeconds: 10, + }, + }, + }, + }, + }, + } + + // configs are applied here + container := &deploy.Spec.Template.Spec.Containers[0] + applyOptionalContainerConfigs(container, serviceConfigs.OptionalConfigs) + + return controllerutil.SetControllerReference(feast.FeatureStore, deploy, feast.Scheme) +} + +func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType) error { + svc.Labels = feast.getLabels(feastType) + deploySettings := FeastServiceConstants[feastType] + + svc.Spec = corev1.ServiceSpec{ + Selector: svc.GetLabels(), + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: strings.ToLower(string(corev1.URISchemeHTTP)), + Port: HttpPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(deploySettings.TargetPort)), + }, + }, + } + + return controllerutil.SetControllerReference(feast.FeatureStore, svc, feast.Scheme) +} + +func (feast *FeastServices) getServiceConfigs(feastType FeastServiceType) feastdevv1alpha1.ServiceConfigs { + appliedSpec := feast.FeatureStore.Status.Applied + if feastType == OfflineFeastType && appliedSpec.Services.OfflineStore != nil { + return appliedSpec.Services.OfflineStore.ServiceConfigs + } + if feastType == OnlineFeastType && appliedSpec.Services.OnlineStore != nil { + return appliedSpec.Services.OnlineStore.ServiceConfigs + } + if feastType == RegistryFeastType && appliedSpec.Services.Registry != nil { + if appliedSpec.Services.Registry.Local != nil { + return appliedSpec.Services.Registry.Local.ServiceConfigs + } + } + return feastdevv1alpha1.ServiceConfigs{} +} + +// GetObjectMeta returns the feast k8s object metadata +func (feast *FeastServices) GetObjectMeta(feastType FeastServiceType) metav1.ObjectMeta { + return metav1.ObjectMeta{Name: feast.GetFeastServiceName(feastType), Namespace: feast.FeatureStore.Namespace} +} + +// GetFeastServiceName returns the feast service object name based on service type +func (feast *FeastServices) GetFeastServiceName(feastType FeastServiceType) string { + return feast.getFeastName() + "-" + string(feastType) +} + +func (feast *FeastServices) getFeastName() string { + return FeastPrefix + feast.FeatureStore.Name +} + +func (feast *FeastServices) getLabels(feastType FeastServiceType) map[string]string { + return map[string]string{ + NameLabelKey: feast.FeatureStore.Name, + ServiceTypeLabelKey: string(feastType), + } +} + +func (feast *FeastServices) setServiceHostnames() error { + feast.FeatureStore.Status.ServiceHostnames = feastdevv1alpha1.ServiceHostnames{} + services := feast.FeatureStore.Status.Applied.Services + if services != nil { + domain := svcDomain + ":" + strconv.Itoa(HttpPort) + if services.OfflineStore != nil { + objMeta := feast.GetObjectMeta(OfflineFeastType) + feast.FeatureStore.Status.ServiceHostnames.OfflineStore = objMeta.Name + "." + objMeta.Namespace + domain + } + if services.OnlineStore != nil { + objMeta := feast.GetObjectMeta(OnlineFeastType) + feast.FeatureStore.Status.ServiceHostnames.OnlineStore = objMeta.Name + "." + objMeta.Namespace + domain + } + if feast.isLocalRegistry() { + objMeta := feast.GetObjectMeta(RegistryFeastType) + feast.FeatureStore.Status.ServiceHostnames.Registry = objMeta.Name + "." + objMeta.Namespace + domain + } else if feast.isRemoteRegistry() { + return feast.setRemoteRegistryURL() + } + } + return nil +} + +func (feast *FeastServices) setFeastServiceCondition(err error, feastType FeastServiceType) error { + conditionMap := FeastServiceConditions[feastType] + if err != nil { + logger := log.FromContext(feast.Context) + cond := conditionMap[metav1.ConditionFalse] + cond.Message = "Error: " + err.Error() + apimeta.SetStatusCondition(&feast.FeatureStore.Status.Conditions, cond) + logger.Error(err, "Error deploying the FeatureStore "+string(ClientFeastType)+" service") + return err + } else { + apimeta.SetStatusCondition(&feast.FeatureStore.Status.Conditions, conditionMap[metav1.ConditionTrue]) + } + return nil +} + +func (feast *FeastServices) setRemoteRegistryURL() error { + if feast.isRemoteHostnameRegistry() { + feast.FeatureStore.Status.ServiceHostnames.Registry = *feast.FeatureStore.Status.Applied.Services.Registry.Remote.Hostname + } else if feast.IsRemoteRefRegistry() { + feastRemoteRef := feast.FeatureStore.Status.Applied.Services.Registry.Remote.FeastRef + // default to FeatureStore namespace if not set + if len(feastRemoteRef.Namespace) == 0 { + feastRemoteRef.Namespace = feast.FeatureStore.Namespace + } + + nsName := types.NamespacedName{Name: feastRemoteRef.Name, Namespace: feastRemoteRef.Namespace} + crNsName := client.ObjectKeyFromObject(feast.FeatureStore) + if nsName == crNsName { + return errors.New("FeatureStore '" + crNsName.Name + "' can't reference itself in `spec.services.registry.remote.feastRef`") + } + + remoteFeastObj := &feastdevv1alpha1.FeatureStore{} + if err := feast.Client.Get(feast.Context, nsName, remoteFeastObj); err != nil { + if apierrors.IsNotFound(err) { + return errors.New("Referenced FeatureStore '" + feastRemoteRef.Name + "' was not found") + } + return err + } + + remoteFeast := FeastServices{ + Client: feast.Client, + Context: feast.Context, + FeatureStore: remoteFeastObj, + Scheme: feast.Scheme, + } + // referenced/remote registry must use the local install option and be in a 'Ready' state. + if remoteFeast.isLocalRegistry() && apimeta.IsStatusConditionTrue(remoteFeastObj.Status.Conditions, feastdevv1alpha1.RegistryReadyType) { + feast.FeatureStore.Status.ServiceHostnames.Registry = remoteFeastObj.Status.ServiceHostnames.Registry + } else { + return errors.New("Remote feast registry of referenced FeatureStore '" + feastRemoteRef.Name + "' is not ready") + } + } + return nil +} + +func (feast *FeastServices) isLocalRegistry() bool { + appliedServices := feast.FeatureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Local != nil +} + +func (feast *FeastServices) isRemoteRegistry() bool { + appliedServices := feast.FeatureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Remote != nil +} + +func (feast *FeastServices) IsRemoteRefRegistry() bool { + if feast.isRemoteRegistry() { + remote := feast.FeatureStore.Status.Applied.Services.Registry.Remote + return remote != nil && remote.FeastRef != nil + } + return false +} + +func (feast *FeastServices) isRemoteHostnameRegistry() bool { + if feast.isRemoteRegistry() { + remote := feast.FeatureStore.Status.Applied.Services.Registry.Remote + return remote != nil && remote.Hostname != nil + } + return false +} + +func (feast *FeastServices) initFeastDeploy(feastType FeastServiceType) *appsv1.Deployment { + deploy := &appsv1.Deployment{ + ObjectMeta: feast.GetObjectMeta(feastType), + } + deploy.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) + return deploy +} + +func (feast *FeastServices) initFeastSvc(feastType FeastServiceType) *corev1.Service { + svc := &corev1.Service{ + ObjectMeta: feast.GetObjectMeta(feastType), + } + svc.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) + return svc +} + +// delete an object if the FeatureStore is set as the object's controller/owner +func (feast *FeastServices) deleteOwnedFeastObj(obj client.Object) error { + if err := feast.Client.Get(feast.Context, client.ObjectKeyFromObject(obj), obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + for _, ref := range obj.GetOwnerReferences() { + if *ref.Controller && ref.UID == feast.FeatureStore.UID { + if err := feast.Client.Delete(feast.Context, obj); err != nil { + return err + } + } + } + return nil +} + +func applyOptionalContainerConfigs(container *corev1.Container, optionalConfigs feastdevv1alpha1.OptionalConfigs) { + if optionalConfigs.ImagePullPolicy != nil { + container.ImagePullPolicy = *optionalConfigs.ImagePullPolicy + } + if optionalConfigs.Resources != nil { + container.Resources = *optionalConfigs.Resources + } +} diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index e1a318a3943..c2348666179 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -19,7 +19,9 @@ package services import ( "context" + "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -27,24 +29,118 @@ import ( const ( FeastPrefix = "feast-" FeatureStoreYamlEnvVar = "FEATURE_STORE_YAML_BASE64" - RegistryPort = int32(6570) + FeatureStoreYamlCmKey = "feature_store.yaml" LocalRegistryPath = "/tmp/registry.db" + LocalOnlinePath = "/tmp/online_store.db" + svcDomain = ".svc.cluster.local" + HttpPort = 80 + OfflineFeastType FeastServiceType = "offline" + OnlineFeastType FeastServiceType = "online" RegistryFeastType FeastServiceType = "registry" ClientFeastType FeastServiceType = "client" + OfflineRemoteConfigType OfflineConfigType = "remote" + OfflineDaskConfigType OfflineConfigType = "dask" + + OnlineRemoteConfigType OnlineConfigType = "remote" + OnlineSqliteConfigType OnlineConfigType = "sqlite" + RegistryRemoteConfigType RegistryConfigType = "remote" RegistryFileConfigType RegistryConfigType = "file" LocalProviderType FeastProviderType = "local" ) +var ( + DefaultImage = "feastdev/feature-server:" + feastversion.FeastVersion + DefaultReplicas = int32(1) + NameLabelKey = feastdevv1alpha1.GroupVersion.Group + "/name" + ServiceTypeLabelKey = feastdevv1alpha1.GroupVersion.Group + "/service-type" + + FeastServiceConstants = map[FeastServiceType]deploymentSettings{ + OfflineFeastType: { + Command: []string{"feast", "serve_offline", "-h", "0.0.0.0"}, + TargetPort: 8815, + }, + OnlineFeastType: { + Command: []string{"feast", "serve", "-h", "0.0.0.0"}, + TargetPort: 6566, + }, + RegistryFeastType: { + Command: []string{"feast", "serve_registry"}, + TargetPort: 6570, + }, + } + + FeastServiceConditions = map[FeastServiceType]map[metav1.ConditionStatus]metav1.Condition{ + OfflineFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1alpha1.OfflineStoreReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.OfflineStoreReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.OfflineStoreReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.OfflineStoreFailedReason, + }, + }, + OnlineFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1alpha1.OnlineStoreReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.OnlineStoreReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.OnlineStoreReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.OnlineStoreFailedReason, + }, + }, + RegistryFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1alpha1.RegistryReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.RegistryReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.RegistryReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.RegistryFailedReason, + }, + }, + ClientFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1alpha1.ClientReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.ClientReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.ClientReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.ClientFailedReason, + }, + }, + } +) + // FeastServiceType is the type of feast service type FeastServiceType string +// OfflineConfigType provider name or a class name that implements Offline Store +type OfflineConfigType string + // RegistryConfigType provider name or a class name that implements Registry type RegistryConfigType string +// OnlineConfigType provider name or a class name that implements Online Store +type OnlineConfigType string + // FeastProviderType defines an implementation of a feature store object type FeastProviderType string @@ -59,10 +155,25 @@ 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"` - Registry RegistryConfig `yaml:"registry,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"` + EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` +} + +// OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline store. +type OfflineStoreConfig struct { + Host string `yaml:"host,omitempty"` + Type OfflineConfigType `yaml:"type,omitempty"` + Port int `yaml:"port,omitempty"` +} + +// OnlineStoreConfig is the configuration that relates to reading from and writing to the Feast online store. +type OnlineStoreConfig struct { + Path string `yaml:"path,omitempty"` + Type OnlineConfigType `yaml:"type,omitempty"` } // RegistryConfig is the configuration that relates to reading from and writing to the Feast registry. @@ -70,3 +181,8 @@ type RegistryConfig struct { Path string `yaml:"path,omitempty"` RegistryType RegistryConfigType `yaml:"registry_type,omitempty"` } + +type deploymentSettings struct { + Command []string + TargetPort int32 +} diff --git a/infra/scripts/release/files_to_bump.txt b/infra/scripts/release/files_to_bump.txt index e1731ae8770..652bc3cad10 100644 --- a/infra/scripts/release/files_to_bump.txt +++ b/infra/scripts/release/files_to_bump.txt @@ -14,6 +14,6 @@ infra/feast-helm-operator/Makefile 6 infra/feast-helm-operator/config/manager/kustomization.yaml 8 infra/feast-operator/Makefile 6 infra/feast-operator/config/manager/kustomization.yaml 8 -infra/feast-operator/api/feastversion/feastversion.go 20 +infra/feast-operator/api/feastversion/version.go 20 java/pom.xml 38 ui/package.json 3