diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 5907ca87206..471db0bfaab 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -28,26 +28,29 @@ const ( FailedPhase = "Failed" // Feast condition types: - ClientReadyType = "Client" - OfflineStoreReadyType = "OfflineStore" - OnlineStoreReadyType = "OnlineStore" - RegistryReadyType = "Registry" - ReadyType = "FeatureStore" + ClientReadyType = "Client" + OfflineStoreReadyType = "OfflineStore" + OnlineStoreReadyType = "OnlineStore" + RegistryReadyType = "Registry" + ReadyType = "FeatureStore" + AuthorizationReadyType = "AuthorizationReadyType" // Feast condition reasons: - ReadyReason = "Ready" - FailedReason = "FeatureStoreFailed" - OfflineStoreFailedReason = "OfflineStoreDeploymentFailed" - OnlineStoreFailedReason = "OnlineStoreDeploymentFailed" - RegistryFailedReason = "RegistryDeploymentFailed" - ClientFailedReason = "ClientDeploymentFailed" + ReadyReason = "Ready" + FailedReason = "FeatureStoreFailed" + OfflineStoreFailedReason = "OfflineStoreDeploymentFailed" + OnlineStoreFailedReason = "OnlineStoreDeploymentFailed" + RegistryFailedReason = "RegistryDeploymentFailed" + ClientFailedReason = "ClientDeploymentFailed" + KubernetesAuthzFailedReason = "KubernetesAuthorizationDeploymentFailed" // Feast condition messages: - ReadyMessage = "FeatureStore installation complete" - OfflineStoreReadyMessage = "Offline Store installation complete" - OnlineStoreReadyMessage = "Online Store 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" + KubernetesAuthzReadyMessage = "Kubernetes authorization installation complete" // entity_key_serialization_version SerializationVersion = 3 @@ -59,6 +62,7 @@ type FeatureStoreSpec struct { // 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"` Services *FeatureStoreServices `json:"services,omitempty"` + AuthzConfig *AuthzConfig `json:"authz,omitempty"` } // FeatureStoreServices defines the desired feast service deployments. ephemeral registry is deployed by default. @@ -263,6 +267,23 @@ type OptionalConfigs struct { Resources *corev1.ResourceRequirements `json:"resources,omitempty"` } +// AuthzConfig defines the authorization settings for the deployed Feast services. +type AuthzConfig struct { + KubernetesAuthz *KubernetesAuthz `json:"kubernetes,omitempty"` +} + +// KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. +// https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +type KubernetesAuthz struct { + // The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + // Roles are managed by the operator and created with an empty list of rules. + // See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + // The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + // This configuration option is only providing a way to automate this procedure. + // Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + Roles []string `json:"roles,omitempty"` +} + // FeatureStoreStatus defines the observed state of FeatureStore type FeatureStoreStatus struct { // Shows the currently applied feast configuration, including any pertinent defaults diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index be0a4201efd..0985e611cd8 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,26 @@ import ( 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 *AuthzConfig) DeepCopyInto(out *AuthzConfig) { + *out = *in + if in.KubernetesAuthz != nil { + in, out := &in.KubernetesAuthz, &out.KubernetesAuthz + *out = new(KubernetesAuthz) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthzConfig. +func (in *AuthzConfig) DeepCopy() *AuthzConfig { + if in == nil { + return nil + } + out := new(AuthzConfig) + in.DeepCopyInto(out) + return out +} + // 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 @@ -158,6 +178,11 @@ func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { *out = new(FeatureStoreServices) (*in).DeepCopyInto(*out) } + if in.AuthzConfig != nil { + in, out := &in.AuthzConfig, &out.AuthzConfig + *out = new(AuthzConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. @@ -194,6 +219,26 @@ func (in *FeatureStoreStatus) DeepCopy() *FeatureStoreStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesAuthz) DeepCopyInto(out *KubernetesAuthz) { + *out = *in + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthz. +func (in *KubernetesAuthz) DeepCopy() *KubernetesAuthz { + if in == nil { + return nil + } + out := new(KubernetesAuthz) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalRegistryConfig) DeepCopyInto(out *LocalRegistryConfig) { *out = *in diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index e4f1357cbaf..b2dd5c0f926 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -48,6 +48,28 @@ spec: spec: description: FeatureStoreSpec defines the desired state of FeatureStore properties: + authz: + description: AuthzConfig defines the authorization settings for the + deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes.io/docs/reference/access-authn-authz/rbac/ + properties: + roles: + description: |- + The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + Roles are managed by the operator and created with an empty list of rules. + See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + This configuration option is only providing a way to automate this procedure. + Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + items: + type: string + type: array + type: object + type: object feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an @@ -1048,6 +1070,28 @@ spec: description: Shows the currently applied feast configuration, including any pertinent defaults properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes.io/docs/reference/access-authn-authz/rbac/ + properties: + roles: + description: |- + The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + Roles are managed by the operator and created with an empty list of rules. + See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + This configuration option is only providing a way to automate this procedure. + Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + items: + type: string + type: array + type: object + type: object feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index a4b0acfc1cf..5e9ed7f7393 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -62,3 +62,14 @@ rules: - get - patch - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - update + - watch diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_all_services_default.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_all_services_default.yaml new file mode 100644 index 00000000000..1dd156378d8 --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_all_services_default.yaml @@ -0,0 +1,14 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: sample-all-default +spec: + feastProject: my_project + services: + onlineStore: + image: 'feastdev/feature-server:0.40.0' + offlineStore: + image: 'feastdev/feature-server:0.40.0' + registry: + local: + image: 'feastdev/feature-server:0.40.0' \ No newline at end of file diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml new file mode 100644 index 00000000000..ed95b41cf47 --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml @@ -0,0 +1,25 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: sample-kubernetes-auth +spec: + feastProject: my_project + services: + onlineStore: + persistence: + file: + path: /data/online_store.db + offlineStore: + persistence: + file: + type: dask + registry: + local: + persistence: + file: + path: /data/registry.db + authz: + kubernetes: + roles: + - reader + - writer diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 2b02676baf6..731a48da65a 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -56,6 +56,28 @@ spec: spec: description: FeatureStoreSpec defines the desired state of FeatureStore properties: + authz: + description: AuthzConfig defines the authorization settings for the + deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes.io/docs/reference/access-authn-authz/rbac/ + properties: + roles: + description: |- + The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + Roles are managed by the operator and created with an empty list of rules. + See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + This configuration option is only providing a way to automate this procedure. + Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + items: + type: string + type: array + type: object + type: object feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an @@ -1056,6 +1078,28 @@ spec: description: Shows the currently applied feast configuration, including any pertinent defaults properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes.io/docs/reference/access-authn-authz/rbac/ + properties: + roles: + description: |- + The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + Roles are managed by the operator and created with an empty list of rules. + See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + This configuration option is only providing a way to automate this procedure. + Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + items: + type: string + type: array + type: object + type: object feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start @@ -2324,6 +2368,17 @@ rules: - get - patch - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - update + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go new file mode 100644 index 00000000000..59747d75bd2 --- /dev/null +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -0,0 +1,220 @@ +package authz + +import ( + "context" + "slices" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" + rbacv1 "k8s.io/api/rbac/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "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 authorization +func (authz *FeastAuthorization) Deploy() error { + authzConfig := authz.Handler.FeatureStore.Status.Applied.AuthzConfig + if authzConfig != nil { + if authzConfig.KubernetesAuthz != nil { + if err := authz.deployKubernetesAuth(authzConfig.KubernetesAuthz); err != nil { + return err + } + } else { + authz.removeOrphanedRoles() + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) + } + } + return nil +} + +func (authz *FeastAuthorization) deployKubernetesAuth(kubernetesAuth *feastdevv1alpha1.KubernetesAuthz) error { + authz.removeOrphanedRoles() + + if err := authz.createFeastRole(); err != nil { + return authz.setFeastKubernetesAuthCondition(err) + } + if err := authz.createFeastRoleBinding(); err != nil { + return authz.setFeastKubernetesAuthCondition(err) + } + + for _, roleName := range kubernetesAuth.Roles { + if err := authz.createAuthRole(roleName); err != nil { + return authz.setFeastKubernetesAuthCondition(err) + } + } + return authz.setFeastKubernetesAuthCondition(nil) +} + +func (authz *FeastAuthorization) removeOrphanedRoles() { + roleList := &rbacv1.RoleList{} + err := authz.Handler.Client.List(context.TODO(), roleList, &client.ListOptions{ + Namespace: authz.Handler.FeatureStore.Namespace, + LabelSelector: labels.SelectorFromSet(authz.getLabels()), + }) + if err != nil { + return + } + + desiredRoles := []string{} + if authz.Handler.FeatureStore.Status.Applied.AuthzConfig.KubernetesAuthz != nil { + desiredRoles = authz.Handler.FeatureStore.Status.Applied.AuthzConfig.KubernetesAuthz.Roles + } + for _, role := range roleList.Items { + roleName := role.Name + if roleName != authz.getFeastRoleName() && !slices.Contains(desiredRoles, roleName) { + _ = authz.Handler.DeleteOwnedFeastObj(authz.initAuthRole(roleName)) + } + } +} + +func (authz *FeastAuthorization) createFeastRole() error { + logger := log.FromContext(authz.Handler.Context) + role := authz.initFeastRole() + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, role, controllerutil.MutateFn(func() error { + return authz.setFeastRole(role) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Role", role.Name, "operation", op) + } + + return nil +} + +func (authz *FeastAuthorization) initFeastRole() *rbacv1.Role { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getFeastRoleName(), Namespace: authz.Handler.FeatureStore.Namespace}, + } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + return role +} + +func (authz *FeastAuthorization) setFeastRole(role *rbacv1.Role) error { + role.Labels = authz.getLabels() + role.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{rbacv1.GroupName}, + Resources: []string{"roles", "rolebindings"}, + Verbs: []string{"get", "list", "watch"}, + }, + } + + return controllerutil.SetControllerReference(authz.Handler.FeatureStore, role, authz.Handler.Scheme) +} + +func (authz *FeastAuthorization) createFeastRoleBinding() error { + logger := log.FromContext(authz.Handler.Context) + roleBinding := authz.initFeastRoleBinding() + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, roleBinding, controllerutil.MutateFn(func() error { + return authz.setFeastRoleBinding(roleBinding) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "RoleBinding", roleBinding.Name, "operation", op) + } + + return nil +} + +func (authz *FeastAuthorization) initFeastRoleBinding() *rbacv1.RoleBinding { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getFeastRoleName(), Namespace: authz.Handler.FeatureStore.Namespace}, + } + roleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + return roleBinding +} + +func (authz *FeastAuthorization) setFeastRoleBinding(roleBinding *rbacv1.RoleBinding) error { + roleBinding.Labels = authz.getLabels() + roleBinding.Subjects = []rbacv1.Subject{} + if authz.Handler.FeatureStore.Status.Applied.Services.OfflineStore != nil { + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: services.GetFeastServiceName(authz.Handler.FeatureStore, services.OfflineFeastType), + Namespace: authz.Handler.FeatureStore.Namespace, + }) + } + if authz.Handler.FeatureStore.Status.Applied.Services.OnlineStore != nil { + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: services.GetFeastServiceName(authz.Handler.FeatureStore, services.OnlineFeastType), + Namespace: authz.Handler.FeatureStore.Namespace, + }) + } + if services.IsLocalRegistry(authz.Handler.FeatureStore) { + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: services.GetFeastServiceName(authz.Handler.FeatureStore, services.RegistryFeastType), + Namespace: authz.Handler.FeatureStore.Namespace, + }) + } + roleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: authz.getFeastRoleName(), + } + + return controllerutil.SetControllerReference(authz.Handler.FeatureStore, roleBinding, authz.Handler.Scheme) +} + +func (authz *FeastAuthorization) createAuthRole(roleName string) error { + logger := log.FromContext(authz.Handler.Context) + role := authz.initAuthRole(roleName) + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, role, controllerutil.MutateFn(func() error { + return authz.setAuthRole(role) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Role", role.Name, "operation", op) + } + + return nil +} + +func (authz *FeastAuthorization) initAuthRole(roleName string) *rbacv1.Role { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: roleName, Namespace: authz.Handler.FeatureStore.Namespace}, + } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + return role +} + +func (authz *FeastAuthorization) setAuthRole(role *rbacv1.Role) error { + role.Labels = authz.getLabels() + role.Rules = []rbacv1.PolicyRule{} + + return controllerutil.SetControllerReference(authz.Handler.FeatureStore, role, authz.Handler.Scheme) +} + +func (authz *FeastAuthorization) getLabels() map[string]string { + return map[string]string{ + services.NameLabelKey: authz.Handler.FeatureStore.Name, + } +} + +func (authz *FeastAuthorization) setFeastKubernetesAuthCondition(err error) error { + if err != nil { + logger := log.FromContext(authz.Handler.Context) + cond := feastKubernetesAuthConditions[metav1.ConditionFalse] + cond.Message = "Error: " + err.Error() + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, cond) + logger.Error(err, "Error deploying the Kubernetes authorization") + return err + } else { + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue]) + } + return nil +} + +func (authz *FeastAuthorization) getFeastRoleName() string { + return GetFeastRoleName(authz.Handler.FeatureStore) +} + +func GetFeastRoleName(featureStore *feastdevv1alpha1.FeatureStore) string { + return services.GetFeastName(featureStore) +} diff --git a/infra/feast-operator/internal/controller/authz/authz_types.go b/infra/feast-operator/internal/controller/authz/authz_types.go new file mode 100644 index 00000000000..f955f5b40f1 --- /dev/null +++ b/infra/feast-operator/internal/controller/authz/authz_types.go @@ -0,0 +1,28 @@ +package authz + +import ( + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FeastAuthorization is an interface for configuring feast authorization +type FeastAuthorization struct { + Handler handler.FeastHandler +} + +var ( + feastKubernetesAuthConditions = map[metav1.ConditionStatus]metav1.Condition{ + metav1.ConditionTrue: { + Type: feastdevv1alpha1.AuthorizationReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.KubernetesAuthzReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.AuthorizationReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.KubernetesAuthzFailedReason, + }, + } +) diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index 278ea4a78fd..b90305b56b7 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -23,6 +23,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,11 +31,13 @@ import ( "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" + handler "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" + feasthandler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -54,6 +57,7 @@ type FeatureStoreReconciler struct { //+kubebuilder:rbac:groups=feast.dev,resources=featurestores/finalizers,verbs=update //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;create;update;watch;delete //+kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims;serviceaccounts,verbs=get;list;create;update;watch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;create;update;watch;delete //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -107,13 +111,15 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Message: feastdevv1alpha1.ReadyMessage, } - feast := services.FeastServices{ - Client: r.Client, - Context: ctx, - FeatureStore: cr, - Scheme: r.Scheme, + authz := authz.FeastAuthorization{ + Handler: feasthandler.FeastHandler{ + Client: r.Client, + Context: ctx, + FeatureStore: cr, + Scheme: r.Scheme, + }, } - if err = feast.Deploy(); err != nil { + if err = authz.Deploy(); err != nil { condition = metav1.Condition{ Type: feastdevv1alpha1.ReadyType, Status: metav1.ConditionFalse, @@ -121,6 +127,23 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Message: "Error: " + err.Error(), } result = ctrl.Result{Requeue: true, RequeueAfter: RequeueDelayError} + } else { + feast := services.FeastServices{ + Handler: feasthandler.FeastHandler{ + Client: r.Client, + Context: ctx, + FeatureStore: cr, + Scheme: r.Scheme, + }} + 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, RequeueAfter: RequeueDelayError} + } } logger.Info(condition.Message) @@ -145,6 +168,8 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.Service{}). Owns(&corev1.PersistentVolumeClaim{}). Owns(&corev1.ServiceAccount{}). + Owns(&rbacv1.RoleBinding{}). + Owns(&rbacv1.Role{}). Watches(&feastdevv1alpha1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)). Complete(r) } @@ -169,11 +194,12 @@ func (r *FeatureStoreReconciler) mapFeastRefsToFeastRequests(ctx context.Context // this if statement is extra protection against any potential infinite reconcile loops if feastRefNsName != objNsName { feast := services.FeastServices{ - Client: r.Client, - Context: ctx, - FeatureStore: &obj, - Scheme: r.Scheme, - } + Handler: feasthandler.FeastHandler{ + 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} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go index 913f022022d..71713eb872f 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -39,6 +39,7 @@ import ( "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -115,15 +116,18 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + 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.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -161,6 +165,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -249,10 +256,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } // check registry config @@ -285,6 +294,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { RegistryType: services.RegistryFileConfigType, Path: registryPath, }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfig).To(Equal(testConfig)) @@ -321,7 +331,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineFilePersistenceDuckDbConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOffline).To(Equal(offlineConfig)) @@ -362,7 +373,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Path: onlineStorePath, Type: services.OnlineSqliteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOnline).To(Equal(onlineConfig)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3)) @@ -388,7 +400,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), Type: services.OnlineRemoteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -408,7 +421,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource = &feastdevv1alpha1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast.FeatureStore = resource + feast.Handler.FeatureStore = resource // check registry config deploy = &appsv1.Deployment{} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go new file mode 100644 index 00000000000..6589e181af7 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go @@ -0,0 +1,566 @@ +/* +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 controller + +import ( + "context" + "encoding/base64" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "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" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "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" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" +) + +var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { + Context("When deploying a resource with all ephemeral services and Kubernetes authorization", func() { + const resourceName = "kubernetes-authorization" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + featurestore := &feastdevv1alpha1.FeatureStore{} + roles := []string{"reader", "writer"} + + BeforeEach(func() { + By("creating the custom resource for the Kind FeatureStore") + err := k8sClient.Get(ctx, typeNamespacedName, featurestore) + if err != nil && errors.IsNotFound(err) { + resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{}) + resource.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{ + Roles: roles, + }} + + 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{ + Handler: handler.FeastHandler{ + 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)) + expectedAuthzConfig := &feastdevv1alpha1.AuthzConfig{ + KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{ + Roles: roles, + }, + } + Expect(resource.Status.Applied.AuthzConfig).To(Equal(expectedAuthzConfig)) + Expect(resource.Status.Applied.Services).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.FilePersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.FilePersistence.Type).To(Equal(string(services.OfflineFilePersistenceDaskConfigType))) + 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.Persistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.FilePersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.FilePersistence.Path).To(Equal(services.DefaultOnlineStoreEphemeralPath)) + Expect(resource.Status.Applied.Services.OnlineStore.Env).To(Equal(&[]corev1.EnvVar{})) + 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.Persistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.Path).To(Equal(services.DefaultRegistryEphemeralPath)) + 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.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.AuthorizationReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.KubernetesAuthzReadyMessage)) + + 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)) + + // check offline deployment + 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.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(0)) + Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(0)) + + // check online deployment + 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.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(0)) + Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(0)) + + // check registry deployment + 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)) + Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(0)) + Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(0)) + + // check configured Roles + for _, roleName := range roles { + role := &rbacv1.Role{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: roleName, + Namespace: resource.Namespace, + }, + role) + Expect(err).NotTo(HaveOccurred()) + Expect(role.Rules).To(BeEmpty()) + } + + // check Feast Role + feastRole := &rbacv1.Role{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: authz.GetFeastRoleName(resource), + Namespace: resource.Namespace, + }, + feastRole) + Expect(err).NotTo(HaveOccurred()) + Expect(feastRole.Rules).ToNot(BeEmpty()) + Expect(feastRole.Rules).To(HaveLen(1)) + Expect(feastRole.Rules[0].APIGroups).To(HaveLen(1)) + Expect(feastRole.Rules[0].APIGroups[0]).To(Equal(rbacv1.GroupName)) + Expect(feastRole.Rules[0].Resources).To(HaveLen(2)) + Expect(feastRole.Rules[0].Resources).To(ContainElement("roles")) + Expect(feastRole.Rules[0].Resources).To(ContainElement("rolebindings")) + Expect(feastRole.Rules[0].Verbs).To(HaveLen(3)) + Expect(feastRole.Rules[0].Verbs).To(ContainElement("get")) + Expect(feastRole.Rules[0].Verbs).To(ContainElement("list")) + Expect(feastRole.Rules[0].Verbs).To(ContainElement("watch")) + + // check RoleBinding + roleBinding := &rbacv1.RoleBinding{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: authz.GetFeastRoleName(resource), + Namespace: resource.Namespace, + }, + roleBinding) + Expect(err).NotTo(HaveOccurred()) + + // check ServiceAccounts + expectedRoleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: feastRole.Name, + } + for _, serviceType := range []services.FeastServiceType{services.RegistryFeastType, services.OnlineFeastType, services.OfflineFeastType} { + sa := &corev1.ServiceAccount{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(serviceType), + Namespace: resource.Namespace, + }, + sa) + Expect(err).NotTo(HaveOccurred()) + + expectedSubject := rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: sa.Name, + Namespace: sa.Namespace, + } + Expect(roleBinding.Subjects).To(ContainElement(expectedSubject)) + Expect(roleBinding.RoleRef).To(Equal(expectedRoleRef)) + } + + By("Updating the user roled and reconciling") + resourceNew := resource.DeepCopy() + rolesNew := roles[1:] + resourceNew.Spec.AuthzConfig.KubernetesAuthz.Roles = rolesNew + err = k8sClient.Update(ctx, resourceNew) + Expect(err).NotTo(HaveOccurred()) + _, 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.Handler.FeatureStore = resource + + // check new Roles + for _, roleName := range rolesNew { + role := &rbacv1.Role{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: roleName, + Namespace: resource.Namespace, + }, + role) + Expect(err).NotTo(HaveOccurred()) + Expect(role.Rules).To(BeEmpty()) + } + + // check deleted Role + role := &rbacv1.Role{} + deletedRole := roles[0] + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: deletedRole, + Namespace: resource.Namespace, + }, + role) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + By("Clearing the kubernetes authorizatino and reconciling") + resourceNew = resource.DeepCopy() + resourceNew.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + err = k8sClient.Update(ctx, resourceNew) + Expect(err).NotTo(HaveOccurred()) + _, 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.Handler.FeatureStore = resource + + // check no Roles + for _, roleName := range roles { + role := &rbacv1.Role{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: roleName, + Namespace: resource.Namespace, + }, + role) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + } + // check no RoleBinding + roleBinding = &rbacv1.RoleBinding{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: authz.GetFeastRoleName(resource), + Namespace: resource.Namespace, + }, + roleBinding) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + 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{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + // check registry deployment + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + // check registry config + 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.DefaultRegistryEphemeralPath, + S3AdditionalKwargs: nil, + }, + AuthzConfig: services.AuthzConfig{ + Type: services.KubernetesAuthType, + }, + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline deployment + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + // check offline config + 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()) + repoConfig = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + } + testConfig = &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineFilePersistenceDaskConfigType, + }, + Registry: regRemote, + AuthzConfig: services.AuthzConfig{ + Type: services.KubernetesAuthType, + }, + } + Expect(repoConfig).To(Equal(testConfig)) + + // check online deployment + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + // check online config + 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()) + repoConfig = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + testConfig = &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Path: services.DefaultOnlineStoreEphemeralPath, + Type: services.OnlineSqliteConfigType, + }, + Registry: regRemote, + AuthzConfig: services.AuthzConfig{ + Type: services.KubernetesAuthType, + }, + } + 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, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), + Type: services.OnlineRemoteConfigType, + }, + Registry: services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + }, + AuthzConfig: services.AuthzConfig{ + Type: services.KubernetesAuthType, + }, + } + Expect(repoConfigClient).To(Equal(clientConfig)) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go index cedcba6124b..dce28f99118 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -38,6 +38,7 @@ import ( "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -110,15 +111,18 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + 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.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) @@ -146,6 +150,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -220,7 +227,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource = &feastdevv1alpha1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast.FeatureStore = resource + feast.Handler.FeatureStore = resource Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.S3AdditionalKwargs).NotTo(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.S3AdditionalKwargs).NotTo(Equal(&s3AdditionalKwargs)) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.S3AdditionalKwargs).To(Equal(&newS3AdditionalKwargs)) @@ -274,10 +281,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } // check registry deployment @@ -312,6 +321,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Path: registryPath, S3AdditionalKwargs: &s3AdditionalKwargs, }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfig).To(Equal(testConfig)) @@ -355,6 +365,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { RegistryType: services.RegistryRemoteConfigType, Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -371,7 +382,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource = &feastdevv1alpha1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast.FeatureStore = resource + feast.Handler.FeatureStore = resource // check registry config deploy = &appsv1.Deployment{} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index f124db55a6c..33fde346385 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -42,6 +42,7 @@ import ( "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -141,15 +142,18 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + 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.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -217,6 +221,9 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -353,7 +360,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource = &feastdevv1alpha1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast.FeatureStore = resource + feast.Handler.FeatureStore = resource Expect(resource.Status.Applied.Services.OnlineStore.Persistence.FilePersistence.PvcConfig).To(BeNil()) // check online deployment @@ -368,7 +375,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(deploy.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(0)) // check online pvc is deleted - log.FromContext(feast.Context).Info("Checking deletion of", "PersistentVolumeClaim", deploy.Name) + log.FromContext(feast.Handler.Context).Info("Checking deletion of", "PersistentVolumeClaim", deploy.Name) pvc = &corev1.PersistentVolumeClaim{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: deploy.Name, @@ -418,10 +425,12 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } // check registry deployment @@ -455,6 +464,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { RegistryType: services.RegistryFileConfigType, Path: registryMountedPath, }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfig).To(Equal(testConfig)) @@ -492,7 +502,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineFilePersistenceDuckDbConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOffline).To(Equal(offlineConfig)) @@ -533,7 +544,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Path: onlineStoreMountedPath, Type: services.OnlineSqliteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOnline).To(Equal(onlineConfig)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3)) @@ -559,7 +571,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), Type: services.OnlineRemoteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -583,7 +596,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource = &feastdevv1alpha1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - feast.FeatureStore = resource + feast.Handler.FeatureStore = resource // check registry config deploy = &appsv1.Deployment{} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 00a6e71c71c..980f3e36f5f 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -39,6 +39,7 @@ import ( "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -118,10 +119,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } Expect(resource.Status).NotTo(BeNil()) Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) @@ -130,6 +133,7 @@ var _ = Describe("FeatureStore Controller", func() { 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.Applied.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) @@ -204,10 +208,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } deploy := &appsv1.Deployment{} @@ -240,6 +246,7 @@ var _ = Describe("FeatureStore Controller", func() { RegistryType: services.RegistryFileConfigType, Path: services.DefaultRegistryEphemeralPath, }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfig).To(Equal(testConfig)) @@ -263,6 +270,7 @@ var _ = Describe("FeatureStore Controller", func() { RegistryType: services.RegistryRemoteConfigType, Path: "feast-test-resource-registry.default.svc.cluster.local:80", }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -319,10 +327,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } deploy := &appsv1.Deployment{} @@ -367,6 +377,9 @@ var _ = Describe("FeatureStore Controller", func() { 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.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) @@ -434,15 +447,18 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + 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.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -480,6 +496,9 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -574,10 +593,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } // check registry config @@ -611,6 +632,7 @@ var _ = Describe("FeatureStore Controller", func() { RegistryType: services.RegistryFileConfigType, Path: services.DefaultRegistryEphemeralPath, }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfig).To(Equal(testConfig)) @@ -648,7 +670,8 @@ var _ = Describe("FeatureStore Controller", func() { OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineFilePersistenceDaskConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOffline).To(Equal(offlineConfig)) @@ -690,7 +713,8 @@ var _ = Describe("FeatureStore Controller", func() { Path: services.DefaultOnlineStoreEphemeralPath, Type: services.OnlineSqliteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigOnline).To(Equal(onlineConfig)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(3)) @@ -716,7 +740,8 @@ var _ = Describe("FeatureStore Controller", func() { Path: "http://feast-services-online.default.svc.cluster.local:80", Type: services.OnlineRemoteConfigType, }, - Registry: regRemote, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -792,10 +817,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cmList.Items).To(HaveLen(1)) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } fsYamlStr := "" @@ -957,6 +984,7 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).To(HaveOccurred()) err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) 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) @@ -972,6 +1000,7 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).To(HaveOccurred()) err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) 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) @@ -989,6 +1018,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) 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()) @@ -997,10 +1027,12 @@ var _ = Describe("FeatureStore Controller", func() { 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, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } // check client config @@ -1030,6 +1062,7 @@ var _ = Describe("FeatureStore Controller", func() { RegistryType: services.RegistryRemoteConfigType, Path: "feast-" + referencedRegistry.Name + "-registry.default.svc.cluster.local:80", }, + AuthzConfig: noAuthzConfig(), } Expect(repoConfigClient).To(Equal(clientConfig)) @@ -1054,6 +1087,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.ServiceHostnames.Registry).To(BeEmpty()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) 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()) @@ -1081,10 +1115,12 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ - Client: controllerReconciler.Client, - Context: ctx, - Scheme: controllerReconciler.Scheme, - FeatureStore: resource, + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, } deploy := &appsv1.Deployment{} @@ -1129,6 +1165,9 @@ var _ = Describe("FeatureStore Controller", func() { 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.AuthorizationReadyType) + Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -1247,6 +1286,12 @@ func getFeatureStoreYamlEnvVar(envs []corev1.EnvVar) *corev1.EnvVar { return nil } +func noAuthzConfig() services.AuthzConfig { + return services.AuthzConfig{ + Type: services.NoAuthAuthType, + } +} + func areEnvVarArraysEqual(arr1 []corev1.EnvVar, arr2 []corev1.EnvVar) bool { if len(arr1) != len(arr2) { return false diff --git a/infra/feast-operator/internal/controller/handler/handler.go b/infra/feast-operator/internal/controller/handler/handler.go new file mode 100644 index 00000000000..73bacffea47 --- /dev/null +++ b/infra/feast-operator/internal/controller/handler/handler.go @@ -0,0 +1,28 @@ +package handler + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// delete an object if the FeatureStore is set as the object's controller/owner +func (handler *FeastHandler) DeleteOwnedFeastObj(obj client.Object) error { + name := obj.GetName() + kind := obj.GetObjectKind().GroupVersionKind().Kind + if err := handler.Client.Get(handler.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 == handler.FeatureStore.UID { + if err := handler.Client.Delete(handler.Context, obj); err != nil { + return err + } + log.FromContext(handler.Context).Info("Successfully deleted", kind, name) + } + } + return nil +} diff --git a/infra/feast-operator/internal/controller/handler/handler_types.go b/infra/feast-operator/internal/controller/handler/handler_types.go new file mode 100644 index 00000000000..5a26776f569 --- /dev/null +++ b/infra/feast-operator/internal/controller/handler/handler_types.go @@ -0,0 +1,20 @@ +package handler + +import ( + "context" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + FeastPrefix = "feast-" +) + +type FeastHandler struct { + client.Client + Context context.Context + Scheme *runtime.Scheme + FeatureStore *feastdevv1alpha1.FeatureStore +} diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index 1befd2df194..e8faa59f21e 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -30,12 +30,12 @@ func (feast *FeastServices) deployClient() error { } func (feast *FeastServices) createClientConfigMap() error { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) cm := &corev1.ConfigMap{ ObjectMeta: feast.GetObjectMeta(ClientFeastType), } cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, cm, controllerutil.MutateFn(func() error { + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, cm, controllerutil.MutateFn(func() error { return feast.setClientConfigMap(cm) })); err != nil { return err @@ -52,6 +52,6 @@ func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { return err } cm.Data = map[string]string{FeatureStoreYamlCmKey: string(clientYaml)} - feast.FeatureStore.Status.ClientConfigMap = cm.Name - return controllerutil.SetControllerReference(feast.FeatureStore, cm, feast.Scheme) + feast.Handler.FeatureStore.Status.ClientConfigMap = cm.Name + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cm, feast.Handler.Scheme) } diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 899a9157d9b..6e8bd5f0482 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -45,14 +45,14 @@ func (feast *FeastServices) getServiceFeatureStoreYaml(feastType FeastServiceTyp } func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) (RepoConfig, error) { - return getServiceRepoConfig(feastType, feast.FeatureStore, feast.extractConfigFromSecret) + return getServiceRepoConfig(feastType, feast.Handler.FeatureStore, feast.extractConfigFromSecret) } func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1alpha1.FeatureStore, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { appliedSpec := featureStore.Status.Applied repoConfig := getClientRepoConfig(featureStore) - isLocalReg := isLocalRegistry(featureStore) + isLocalRegistry := IsLocalRegistry(featureStore) if appliedSpec.Services != nil { services := appliedSpec.Services @@ -75,7 +75,7 @@ func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1al } case RegistryFeastType: // Registry server only has a `registry` section - if isLocalReg { + if isLocalRegistry { err := setRepoConfigRegistry(services, secretExtractionFunc, &repoConfig) if err != nil { return repoConfig, err @@ -203,7 +203,7 @@ func setRepoConfigOffline(services *feastdevv1alpha1.FeatureStoreServices, secre } func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { - return yaml.Marshal(getClientRepoConfig(feast.FeatureStore)) + return yaml.Marshal(getClientRepoConfig(feast.Handler.FeatureStore)) } func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig { @@ -232,6 +232,18 @@ func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig Path: status.ServiceHostnames.Registry, } } + + if status.Applied.AuthzConfig.KubernetesAuthz == nil { + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: NoAuthAuthType, + } + } else { + if status.Applied.AuthzConfig.KubernetesAuthz != nil { + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: KubernetesAuthType, + } + } + } return clientRepoConfig } diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index cb3c4dabfe9..1a87b118ee5 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -39,18 +39,21 @@ var _ = Describe("Repo Config", func() { var repoConfig RepoConfig repoConfig, err := getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) expectedRegistryConfig := RegistryConfig{ @@ -75,18 +78,21 @@ var _ = Describe("Repo Config", func() { ApplyDefaultsToStatus(featureStore) repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) expectedRegistryConfig = RegistryConfig{ @@ -110,18 +116,21 @@ var _ = Describe("Repo Config", func() { ApplyDefaultsToStatus(featureStore) repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) @@ -156,6 +165,7 @@ var _ = Describe("Repo Config", func() { ApplyDefaultsToStatus(featureStore) repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) expectedOfflineConfig := OfflineStoreConfig{ Type: "duckdb", } @@ -165,6 +175,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) expectedOnlineConfig := OnlineStoreConfig{ Type: "sqlite", @@ -175,6 +186,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) expectedRegistryConfig = RegistryConfig{ @@ -183,6 +195,63 @@ var _ = Describe("Repo Config", func() { } Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + By("Having kubernetes authorization") + featureStore = minimalFeatureStore() + featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ + KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{}, + } + featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{}, + }, + }, + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{}, + }, + }, + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{}, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, mockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) + expectedOfflineConfig = OfflineStoreConfig{ + Type: "dask", + } + Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) + Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) + Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) + + repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, mockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) + Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) + expectedOnlineConfig = OnlineStoreConfig{ + Type: "sqlite", + Path: DefaultOnlineStoreEphemeralPath, + } + Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) + Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) + + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, mockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) + Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) + Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) + expectedRegistryConfig = RegistryConfig{ + RegistryType: "file", + Path: DefaultRegistryEphemeralPath, + } + Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + By("Having the all the db services") featureStore = minimalFeatureStore() featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 7974ee997bd..55cb7079816 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -22,6 +22,7 @@ import ( "strings" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -40,7 +41,7 @@ func (feast *FeastServices) Deploy() error { return err } - services := feast.FeatureStore.Status.Applied.Services + services := feast.Handler.FeatureStore.Status.Applied.Services if services != nil { if services.OfflineStore != nil { offlinePersistence := services.OfflineStore.Persistence @@ -171,12 +172,12 @@ func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence * } func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) error { - if pvcCreate, shouldCreate := shouldCreatePvc(feast.FeatureStore, feastType); shouldCreate { + if pvcCreate, shouldCreate := shouldCreatePvc(feast.Handler.FeatureStore, feastType); shouldCreate { if err := feast.createPVC(pvcCreate, feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) } } else { - _ = feast.deleteOwnedFeastObj(feast.initPVC(feastType)) + _ = feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)) } if err := feast.createServiceAccount(feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) @@ -191,26 +192,26 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) } func (feast *FeastServices) removeFeastServiceByType(feastType FeastServiceType) error { - if err := feast.deleteOwnedFeastObj(feast.initFeastSvc(feastType)); err != nil { + if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastSvc(feastType)); err != nil { return err } - if err := feast.deleteOwnedFeastObj(feast.initFeastDeploy(feastType)); err != nil { + if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastDeploy(feastType)); err != nil { return err } - if err := feast.deleteOwnedFeastObj(feast.initFeastSA(feastType)); err != nil { + if err := feast.Handler.DeleteOwnedFeastObj(feast.initFeastSA(feastType)); err != nil { return err } - if err := feast.deleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { + if err := feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)); err != nil { return err } - apimeta.RemoveStatusCondition(&feast.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) + apimeta.RemoveStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, FeastServiceConditions[feastType][metav1.ConditionTrue].Type) return nil } func (feast *FeastServices) createService(feastType FeastServiceType) error { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) svc := feast.initFeastSvc(feastType) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, svc, controllerutil.MutateFn(func() error { + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, svc, controllerutil.MutateFn(func() error { return feast.setService(svc, feastType) })); err != nil { return err @@ -221,9 +222,9 @@ func (feast *FeastServices) createService(feastType FeastServiceType) error { } func (feast *FeastServices) createServiceAccount(feastType FeastServiceType) error { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) sa := feast.initFeastSA(feastType) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, sa, controllerutil.MutateFn(func() error { + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, sa, controllerutil.MutateFn(func() error { return feast.setServiceAccount(sa, feastType) })); err != nil { return err @@ -234,9 +235,9 @@ func (feast *FeastServices) createServiceAccount(feastType FeastServiceType) err } func (feast *FeastServices) createDeployment(feastType FeastServiceType) error { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) deploy := feast.initFeastDeploy(feastType) - if op, err := controllerutil.CreateOrUpdate(feast.Context, feast.Client, deploy, controllerutil.MutateFn(func() error { + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, deploy, controllerutil.MutateFn(func() error { return feast.setDeployment(deploy, feastType) })); err != nil { return err @@ -248,15 +249,15 @@ func (feast *FeastServices) createDeployment(feastType FeastServiceType) error { } func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, feastType FeastServiceType) error { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) pvc, err := feast.createNewPVC(pvcCreate, feastType) if err != nil { return err } - err = feast.Client.Get(feast.Context, client.ObjectKeyFromObject(pvc), pvc) + err = feast.Handler.Client.Get(feast.Handler.Context, client.ObjectKeyFromObject(pvc), pvc) if err != nil && apierrors.IsNotFound(err) { - err = feast.Client.Create(feast.Context, pvc) + err = feast.Handler.Client.Create(feast.Handler.Context, pvc) if err != nil { return err } @@ -329,11 +330,11 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType F // configs are applied here container := &deploy.Spec.Template.Spec.Containers[0] applyOptionalContainerConfigs(container, serviceConfigs.OptionalConfigs) - if pvcConfig, hasPvcConfig := hasPvcConfig(feast.FeatureStore, feastType); hasPvcConfig { + if pvcConfig, hasPvcConfig := hasPvcConfig(feast.Handler.FeatureStore, feastType); hasPvcConfig { mountPvcConfig(&deploy.Spec.Template.Spec, pvcConfig, deploy.Name) } - return controllerutil.SetControllerReference(feast.FeatureStore, deploy, feast.Scheme) + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, deploy, feast.Handler.Scheme) } func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType) error { @@ -353,12 +354,12 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi }, } - return controllerutil.SetControllerReference(feast.FeatureStore, svc, feast.Scheme) + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, svc, feast.Handler.Scheme) } func (feast *FeastServices) setServiceAccount(sa *corev1.ServiceAccount, feastType FeastServiceType) error { sa.Labels = feast.getLabels(feastType) - return controllerutil.SetControllerReference(feast.FeatureStore, sa, feast.Scheme) + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme) } func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1alpha1.PvcCreate, feastType FeastServiceType) (*corev1.PersistentVolumeClaim, error) { @@ -371,11 +372,11 @@ func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1alpha1.PvcCreate, if pvcCreate.StorageClassName != nil { pvc.Spec.StorageClassName = pvcCreate.StorageClassName } - return pvc, controllerutil.SetControllerReference(feast.FeatureStore, pvc, feast.Scheme) + return pvc, controllerutil.SetControllerReference(feast.Handler.FeatureStore, pvc, feast.Handler.Scheme) } func (feast *FeastServices) getServiceConfigs(feastType FeastServiceType) feastdevv1alpha1.ServiceConfigs { - appliedSpec := feast.FeatureStore.Status.Applied + appliedSpec := feast.Handler.FeatureStore.Status.Applied switch feastType { case OfflineFeastType: if appliedSpec.Services.OfflineStore != nil { @@ -397,41 +398,45 @@ func (feast *FeastServices) getServiceConfigs(feastType FeastServiceType) feastd // 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} + return metav1.ObjectMeta{Name: feast.GetFeastServiceName(feastType), Namespace: feast.Handler.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) + return GetFeastServiceName(feast.Handler.FeatureStore, feastType) +} + +// GetFeastServiceName returns the feast service object name based on service type +func GetFeastServiceName(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastServiceType) string { + return GetFeastName(featureStore) + "-" + string(feastType) } -func (feast *FeastServices) getFeastName() string { - return FeastPrefix + feast.FeatureStore.Name +func GetFeastName(featureStore *feastdevv1alpha1.FeatureStore) string { + return handler.FeastPrefix + featureStore.Name } func (feast *FeastServices) getLabels(feastType FeastServiceType) map[string]string { return map[string]string{ - NameLabelKey: feast.FeatureStore.Name, + NameLabelKey: feast.Handler.FeatureStore.Name, ServiceTypeLabelKey: string(feastType), } } func (feast *FeastServices) setServiceHostnames() error { - feast.FeatureStore.Status.ServiceHostnames = feastdevv1alpha1.ServiceHostnames{} - services := feast.FeatureStore.Status.Applied.Services + feast.Handler.FeatureStore.Status.ServiceHostnames = feastdevv1alpha1.ServiceHostnames{} + services := feast.Handler.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 + feast.Handler.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 + feast.Handler.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 + feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = objMeta.Name + "." + objMeta.Namespace + domain } else if feast.isRemoteRegistry() { return feast.setRemoteRegistryURL() } @@ -442,36 +447,36 @@ func (feast *FeastServices) setServiceHostnames() error { func (feast *FeastServices) setFeastServiceCondition(err error, feastType FeastServiceType) error { conditionMap := FeastServiceConditions[feastType] if err != nil { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) cond := conditionMap[metav1.ConditionFalse] cond.Message = "Error: " + err.Error() - apimeta.SetStatusCondition(&feast.FeatureStore.Status.Conditions, cond) + apimeta.SetStatusCondition(&feast.Handler.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]) + apimeta.SetStatusCondition(&feast.Handler.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 + feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = *feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote.Hostname } else if feast.IsRemoteRefRegistry() { - feastRemoteRef := feast.FeatureStore.Status.Applied.Services.Registry.Remote.FeastRef + feastRemoteRef := feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote.FeastRef // default to FeatureStore namespace if not set if len(feastRemoteRef.Namespace) == 0 { - feastRemoteRef.Namespace = feast.FeatureStore.Namespace + feastRemoteRef.Namespace = feast.Handler.FeatureStore.Namespace } nsName := types.NamespacedName{Name: feastRemoteRef.Name, Namespace: feastRemoteRef.Namespace} - crNsName := client.ObjectKeyFromObject(feast.FeatureStore) + crNsName := client.ObjectKeyFromObject(feast.Handler.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 err := feast.Handler.Client.Get(feast.Handler.Context, nsName, remoteFeastObj); err != nil { if apierrors.IsNotFound(err) { return errors.New("Referenced FeatureStore '" + feastRemoteRef.Name + "' was not found") } @@ -479,14 +484,16 @@ func (feast *FeastServices) setRemoteRegistryURL() error { } remoteFeast := FeastServices{ - Client: feast.Client, - Context: feast.Context, - FeatureStore: remoteFeastObj, - Scheme: feast.Scheme, + Handler: handler.FeastHandler{ + Client: feast.Handler.Client, + Context: feast.Handler.Context, + FeatureStore: remoteFeastObj, + Scheme: feast.Handler.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 + feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = remoteFeastObj.Status.ServiceHostnames.Registry } else { return errors.New("Remote feast registry of referenced FeatureStore '" + feastRemoteRef.Name + "' is not ready") } @@ -495,17 +502,17 @@ func (feast *FeastServices) setRemoteRegistryURL() error { } func (feast *FeastServices) isLocalRegistry() bool { - return isLocalRegistry(feast.FeatureStore) + return IsLocalRegistry(feast.Handler.FeatureStore) } func (feast *FeastServices) isRemoteRegistry() bool { - appliedServices := feast.FeatureStore.Status.Applied.Services + appliedServices := feast.Handler.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 + remote := feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote return remote != nil && remote.FeastRef != nil } return false @@ -513,7 +520,7 @@ func (feast *FeastServices) IsRemoteRefRegistry() bool { func (feast *FeastServices) isRemoteHostnameRegistry() bool { if feast.isRemoteRegistry() { - remote := feast.FeatureStore.Status.Applied.Services.Registry.Remote + remote := feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote return remote != nil && remote.Hostname != nil } return false @@ -551,27 +558,6 @@ func (feast *FeastServices) initPVC(feastType FeastServiceType) *corev1.Persiste return pvc } -// delete an object if the FeatureStore is set as the object's controller/owner -func (feast *FeastServices) deleteOwnedFeastObj(obj client.Object) error { - name := obj.GetName() - kind := obj.GetObjectKind().GroupVersionKind().Kind - 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 - } - log.FromContext(feast.Context).Info("Successfully deleted", kind, name) - } - } - return nil -} - func applyOptionalContainerConfigs(container *corev1.Container, optionalConfigs feastdevv1alpha1.OptionalConfigs) { if optionalConfigs.Env != nil { container.Env = mergeEnvVarsArrays(container.Env, optionalConfigs.Env) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 1251a2ff9e3..2e5b1f1a4e5 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -17,17 +17,13 @@ limitations under the License. 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" + handler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" ) const ( - FeastPrefix = "feast-" FeatureStoreYamlEnvVar = "FEATURE_STORE_YAML_BASE64" FeatureStoreYamlCmKey = "feature_store.yaml" DefaultRegistryEphemeralPath = "/tmp/registry.db" @@ -62,6 +58,9 @@ const ( RegistryDBPersistenceSQLConfigType RegistryConfigType = "sql" LocalProviderType FeastProviderType = "local" + + NoAuthAuthType AuthzType = "no_auth" + KubernetesAuthType AuthzType = "kubernetes" ) var ( @@ -141,6 +140,9 @@ var ( } ) +// AuthzType defines the authorization type +type AuthzType string + // FeastServiceType is the type of feast service type FeastServiceType string @@ -158,10 +160,7 @@ type FeastProviderType string // FeastServices is an interface for configuring and deploying feast services type FeastServices struct { - client.Client - Context context.Context - Scheme *runtime.Scheme - FeatureStore *feastdevv1alpha1.FeatureStore + Handler handler.FeastHandler } // RepoConfig is the Repo config. Typically loaded from feature_store.yaml. @@ -172,6 +171,7 @@ type RepoConfig struct { OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` Registry RegistryConfig `yaml:"registry,omitempty"` + AuthzConfig AuthzConfig `yaml:"auth,omitempty"` EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` } @@ -198,6 +198,11 @@ type RegistryConfig struct { DBParameters map[string]interface{} `yaml:",inline,omitempty"` } +// AuthzConfig is the RBAC authorization configuration. +type AuthzConfig struct { + Type AuthzType `yaml:"type,omitempty"` +} + type deploymentSettings struct { Command []string TargetPort int32 diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 5e2daee6738..8e6df6ee667 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -17,7 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -func isLocalRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { +func IsLocalRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { appliedServices := featureStore.Status.Applied.Services return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Local != nil } @@ -35,7 +35,7 @@ func hasPvcConfig(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastSe pvcConfig = services.OfflineStore.Persistence.FilePersistence.PvcConfig } case RegistryFeastType: - if isLocalRegistry(featureStore) && services.Registry.Local.Persistence.FilePersistence != nil { + if IsLocalRegistry(featureStore) && services.Registry.Local.Persistence.FilePersistence != nil { pvcConfig = services.Registry.Local.Persistence.FilePersistence.PvcConfig } } @@ -52,6 +52,11 @@ func shouldCreatePvc(featureStore *feastdevv1alpha1.FeatureStore, feastType Feas func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { cr.Status.FeastVersion = feastversion.FeastVersion applied := cr.Spec.DeepCopy() + + if applied.AuthzConfig == nil { + applied.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + } + if applied.Services == nil { applied.Services = &feastdevv1alpha1.FeatureStoreServices{} } @@ -201,11 +206,11 @@ func checkRegistryDBStorePersistenceType(value string) error { } func (feast *FeastServices) getSecret(secretRef string) (*corev1.Secret, error) { - secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretRef, Namespace: feast.FeatureStore.Namespace}} + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretRef, Namespace: feast.Handler.FeatureStore.Namespace}} objectKey := client.ObjectKeyFromObject(secret) - if err := feast.Client.Get(feast.Context, objectKey, secret); err != nil { + if err := feast.Handler.Client.Get(feast.Handler.Context, objectKey, secret); err != nil { if apierrors.IsNotFound(err) || err != nil { - logger := log.FromContext(feast.Context) + logger := log.FromContext(feast.Handler.Context) logger.Error(err, "invalid secret "+secretRef+" for offline store") return nil, err diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 16af55b03be..2819cb24243 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -377,5 +377,11 @@ var _ = Describe("FeatureStore API", func() { storage = resource.Status.Applied.Services.Registry.Local.Persistence.FilePersistence.PvcConfig.Create.Resources.Requests.Storage().String() Expect(storage).To(Equal("500Mi")) }) + It("should set the default AuthzConfig", func() { + resource := featurestore + services.ApplyDefaultsToStatus(resource) + Expect(resource.Status.Applied.AuthzConfig).ToNot(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).To(Equal(&feastdevv1alpha1.AuthzConfig{})) + }) }) })