From aefebd5b07a263a16311018da03df50b73aaeaa0 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 09:42:28 -0500 Subject: [PATCH 01/40] Added secret and registry to sample yaml Signed-off-by: Theodor Mihalache --- .../v1alpha1_featurestore_db_persistence.yaml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml index 0540bc90ccf..f99815d7100 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml @@ -11,5 +11,25 @@ spec: store: type: postgres secretRef: - name: my-secret - secretKeyName: mykey # optional + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional + registry: + local: + persistence: + store: + type: sql + secretRef: + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret +stringData: + postgres-secret-parameters: | + path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true \ No newline at end of file From 00914b9c62db7ea1ea0d8ddb3de448454528bc95 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:27:26 -0500 Subject: [PATCH 02/40] - Added missing go operator test file - Added tests for invalid store type for each of the feast service Signed-off-by: Theodor Mihalache --- .../featurestore_controller_db_store_test.go | 713 ++++++++++++++++++ .../test/api/featurestore_types_test.go | 54 ++ 2 files changed, 767 insertions(+) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go new file mode 100644 index 00000000000..5badd4bf19b --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -0,0 +1,713 @@ +/* +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/feast-dev/feast/infra/feast-operator/internal/controller/handler" + + . "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" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "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/services" +) + +var cassandraYamlString = ` +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var snowflakeYamlString = ` +account: snowflake_deployment.us-east-1 +user: user_login +password: user_password +role: SYSADMIN +warehouse: COMPUTE_WH +database: FEAST +schema: PUBLIC +` + +var sqlTypeYamlString = ` +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var invalidSecretTypeYamlString = ` +type: cassandra +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var invalidSecretRegistryTypeYamlString = ` +registry_type: sql +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var _ = Describe("FeatureStore Controller - db storage services", func() { + Context("When deploying a resource with all db storage services", func() { + const resourceName = "cr-name" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + offlineSecretNamespacedName := types.NamespacedName{ + Name: "offline-store-secret", + Namespace: "default", + } + + onlineSecretNamespacedName := types.NamespacedName{ + Name: "online-store-secret", + Namespace: "default", + } + + registrySecretNamespacedName := types.NamespacedName{ + Name: "registry-store-secret", + Namespace: "default", + } + + featurestore := &feastdevv1alpha1.FeatureStore{} + offlineType := services.OfflineDBPersistenceSnowflakeConfigType + onlineType := services.OnlineDBPersistenceCassandraConfigType + registryType := services.RegistryDBPersistenceSQLConfigType + + BeforeEach(func() { + By("creating secrets for db stores for custom resource of Kind FeatureStore") + secret := &corev1.Secret{} + + secretData := map[string][]byte{ + string(offlineType): []byte(snowflakeYamlString), + } + err := k8sClient.Get(ctx, offlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: offlineSecretNamespacedName.Name, + Namespace: offlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + string(onlineType): []byte(cassandraYamlString), + } + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: onlineSecretNamespacedName.Name, + Namespace: onlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + "sql_custom_registry_key": []byte(sqlTypeYamlString), + } + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: registrySecretNamespacedName.Name, + Namespace: registrySecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + 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.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: string(offlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "offline-store-secret", + }, + }, + } + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: string(onlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "online-store-secret", + }, + }, + } + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: string(registryType), + SecretRef: corev1.LocalObjectReference{ + Name: "registry-store-secret", + }, + SecretKeyName: "sql_custom_registry_key", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + onlineSecret := &corev1.Secret{} + err := k8sClient.Get(ctx, onlineSecretNamespacedName, onlineSecret) + Expect(err).NotTo(HaveOccurred()) + + offlineSecret := &corev1.Secret{} + err = k8sClient.Get(ctx, offlineSecretNamespacedName, offlineSecret) + Expect(err).NotTo(HaveOccurred()) + + registrySecret := &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, registrySecret) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the secrets") + Expect(k8sClient.Delete(ctx, onlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, offlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, registrySecret)).To(Succeed()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should fail reconciling the resource", func() { + By("Referring to a secret that doesn't exist") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "invalid_secret"} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secrets \"invalid_secret\" not found")) + + By("Referring to a secret with a key that doesn't exist") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key invalid.secret.key doesn't exist in secret online-store-secret")) + + By("Referring to a secret that contains parameter named type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named registry_type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(cassandraYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data["sql_custom_registry_key"] = nil + secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key sql in secret registry-store-secret contains invalid tag named registry_type")) + }) + + 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)) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) + 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.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + svc := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + }) + + It("should properly encode a feature_store.yaml config", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + dbParametersMap := unmarshallYamlString(sqlTypeYamlString) + copyMap := services.CopyMap(dbParametersMap) + delete(dbParametersMap, "path") + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + Path: copyMap["path"].(string), + RegistryType: services.RegistryDBPersistenceSQLConfigType, + DBParameters: dbParametersMap, + }, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineDBPersistenceSnowflakeConfigType, + DBParameters: unmarshallYamlString(snowflakeYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Type: onlineType, + DBParameters: unmarshallYamlString(cassandraYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + + // 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: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change paths and reconcile + resourceNew := resource.DeepCopy() + newOnlineSecretName := "offline-store-secret" + newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = string(services.OfflineDBPersistenceSnowflakeConfigType) + 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 online config + 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()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + + repoConfigOnline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + onlineConfig.OnlineStore.Type = services.OnlineDBPersistenceSnowflakeConfigType + onlineConfig.OnlineStore.DBParameters = unmarshallYamlString(snowflakeYamlString) + Expect(repoConfigOnline).To(Equal(onlineConfig)) + }) + }) +}) + +func unmarshallYamlString(yamlString string) map[string]interface{} { + var parameters map[string]interface{} + + err := yaml.Unmarshal([]byte(yamlString), ¶meters) + if err != nil { + fmt.Println(err) + } + return parameters +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 2819cb24243..2ab93056c93 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -272,6 +272,50 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } +func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + }, + } + return fsCopy +} + const resourceName = "test-resource" const namespaceName = "default" @@ -313,6 +357,10 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("s3://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("gs://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") }) + + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.online\", \"redis\", \"ikv\", \"datastore\", \"dynamodb\", \"bigtable\", \"postgres\", \"cassandra\", \"mysql\", \"hazelcast\", \"singlestore\"") + }) }) Context("When creating an invalid Offline Store", func() { @@ -321,6 +369,9 @@ var _ = Describe("FeatureStore API", func() { It("should fail when PVC persistence has absolute path", func() { attemptInvalidCreationAndAsserts(ctx, offlineStoreWithUnmanagedFileType(featurestore), "Unsupported value") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.offline\", \"bigquery\", \"redshift\", \"spark\", \"postgres\", \"feast_trino.trino.TrinoOfflineStore\", \"redis\"") + }) }) Context("When creating an invalid Registry", func() { @@ -340,6 +391,9 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForFile(featurestore), "Additional S3 settings are available only for S3 object store URIs") attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"sql\", \"snowflake.registry\"") + }) }) Context("When creating an invalid PvcConfig", func() { From fc48971493c05341eed459d845002204cd1e7824 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:43:40 -0500 Subject: [PATCH 03/40] Added a description for SecretKeyName and the default behaviour Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 471db0bfaab..50b71871539 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -100,9 +100,11 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOfflineStoreDBStorePersistenceTypes = []string{ @@ -140,9 +142,11 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOnlineStoreDBStorePersistenceTypes = []string{ @@ -185,9 +189,11 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidRegistryDBStorePersistenceTypes = []string{ From 1c6f1e799167db6f1de28e95ba3b90715b6df11f Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 12:13:57 -0500 Subject: [PATCH 04/40] Added a test where the secret contains invalid type Signed-off-by: Theodor Mihalache --- .../featurestore_controller_db_store_test.go | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 5badd4bf19b..547edbc5181 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -20,8 +20,8 @@ import ( "context" "encoding/base64" "fmt" - "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" @@ -78,7 +78,7 @@ sqlalchemy_config_kwargs: pool_pre_ping: true ` -var invalidSecretTypeYamlString = ` +var invalidSecretContainingTypeYamlString = ` type: cassandra hosts: - 192.168.1.1 @@ -96,6 +96,24 @@ read_concurrency: 100 write_concurrency: 100 ` +var invalidSecretTypeYamlString = ` +type: wrong +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var invalidSecretRegistryTypeYamlString = ` registry_type: sql path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast @@ -295,6 +313,31 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret := &corev1.Secret{} err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretContainingTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named type with invalid value") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) From 83d172066fb4f919b85c6ef3758cd8f149631e60 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 12:13:57 -0500 Subject: [PATCH 05/40] Added a test where the secret contains invalid type Signed-off-by: Theodor Mihalache --- .../crd/bases/feast.dev_featurestores.yaml | 18 +++++++ infra/feast-operator/dist/install.yaml | 18 +++++++ .../featurestore_controller_db_store_test.go | 47 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) 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 b2dd5c0f926..909cc361c66 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,6 +297,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -611,6 +614,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -946,6 +952,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1321,6 +1330,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1640,6 +1652,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1983,6 +1998,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 731a48da65a..9f1341e295d 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,6 +305,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -619,6 +622,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -954,6 +960,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1329,6 +1338,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1648,6 +1660,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1991,6 +2006,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 5badd4bf19b..547edbc5181 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -20,8 +20,8 @@ import ( "context" "encoding/base64" "fmt" - "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" @@ -78,7 +78,7 @@ sqlalchemy_config_kwargs: pool_pre_ping: true ` -var invalidSecretTypeYamlString = ` +var invalidSecretContainingTypeYamlString = ` type: cassandra hosts: - 192.168.1.1 @@ -96,6 +96,24 @@ read_concurrency: 100 write_concurrency: 100 ` +var invalidSecretTypeYamlString = ` +type: wrong +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var invalidSecretRegistryTypeYamlString = ` registry_type: sql path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast @@ -295,6 +313,31 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret := &corev1.Secret{} err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretContainingTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named type with invalid value") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) From ce7d3fdf2ff86fbcb8cddbe89816b6e806533d35 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:38:24 -0500 Subject: [PATCH 06/40] Updated the description of SecretKeyName and SecretRef parameters in the CRD Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 18 ++--- .../crd/bases/feast.dev_featurestores.yaml | 70 +++++++++---------- infra/feast-operator/dist/install.yaml | 70 +++++++++---------- 3 files changed, 77 insertions(+), 81 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 50b71871539..2e89c78d50b 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -100,10 +100,10 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -142,10 +142,10 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -189,10 +189,10 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } 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 909cc361c66..96b2f6073b7 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,14 +297,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -614,14 +613,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -952,14 +950,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1330,14 +1328,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1652,14 +1650,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1998,14 +1996,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 9f1341e295d..6a618b97891 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,14 +305,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -622,14 +621,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -960,14 +958,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1338,14 +1336,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1660,14 +1658,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2006,14 +2004,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- From 7e1b90153a0701bd24ed0c1dc1d719f683833dd4 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 09:42:28 -0500 Subject: [PATCH 07/40] Added secret and registry to sample yaml Signed-off-by: Theodor Mihalache --- .../v1alpha1_featurestore_db_persistence.yaml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml index 0540bc90ccf..f99815d7100 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml @@ -11,5 +11,25 @@ spec: store: type: postgres secretRef: - name: my-secret - secretKeyName: mykey # optional + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional + registry: + local: + persistence: + store: + type: sql + secretRef: + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret +stringData: + postgres-secret-parameters: | + path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true \ No newline at end of file From 7d50c1793ef8bb532a6e85bb3e2d38d4d1f2973b Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:27:26 -0500 Subject: [PATCH 08/40] - Added missing go operator test file - Added tests for invalid store type for each of the feast service Signed-off-by: Theodor Mihalache --- .../featurestore_controller_db_store_test.go | 713 ++++++++++++++++++ .../test/api/featurestore_types_test.go | 54 ++ 2 files changed, 767 insertions(+) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go new file mode 100644 index 00000000000..5badd4bf19b --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -0,0 +1,713 @@ +/* +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/feast-dev/feast/infra/feast-operator/internal/controller/handler" + + . "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" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "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/services" +) + +var cassandraYamlString = ` +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var snowflakeYamlString = ` +account: snowflake_deployment.us-east-1 +user: user_login +password: user_password +role: SYSADMIN +warehouse: COMPUTE_WH +database: FEAST +schema: PUBLIC +` + +var sqlTypeYamlString = ` +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var invalidSecretTypeYamlString = ` +type: cassandra +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var invalidSecretRegistryTypeYamlString = ` +registry_type: sql +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var _ = Describe("FeatureStore Controller - db storage services", func() { + Context("When deploying a resource with all db storage services", func() { + const resourceName = "cr-name" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + offlineSecretNamespacedName := types.NamespacedName{ + Name: "offline-store-secret", + Namespace: "default", + } + + onlineSecretNamespacedName := types.NamespacedName{ + Name: "online-store-secret", + Namespace: "default", + } + + registrySecretNamespacedName := types.NamespacedName{ + Name: "registry-store-secret", + Namespace: "default", + } + + featurestore := &feastdevv1alpha1.FeatureStore{} + offlineType := services.OfflineDBPersistenceSnowflakeConfigType + onlineType := services.OnlineDBPersistenceCassandraConfigType + registryType := services.RegistryDBPersistenceSQLConfigType + + BeforeEach(func() { + By("creating secrets for db stores for custom resource of Kind FeatureStore") + secret := &corev1.Secret{} + + secretData := map[string][]byte{ + string(offlineType): []byte(snowflakeYamlString), + } + err := k8sClient.Get(ctx, offlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: offlineSecretNamespacedName.Name, + Namespace: offlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + string(onlineType): []byte(cassandraYamlString), + } + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: onlineSecretNamespacedName.Name, + Namespace: onlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + "sql_custom_registry_key": []byte(sqlTypeYamlString), + } + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: registrySecretNamespacedName.Name, + Namespace: registrySecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + 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.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: string(offlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "offline-store-secret", + }, + }, + } + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: string(onlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "online-store-secret", + }, + }, + } + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: string(registryType), + SecretRef: corev1.LocalObjectReference{ + Name: "registry-store-secret", + }, + SecretKeyName: "sql_custom_registry_key", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + onlineSecret := &corev1.Secret{} + err := k8sClient.Get(ctx, onlineSecretNamespacedName, onlineSecret) + Expect(err).NotTo(HaveOccurred()) + + offlineSecret := &corev1.Secret{} + err = k8sClient.Get(ctx, offlineSecretNamespacedName, offlineSecret) + Expect(err).NotTo(HaveOccurred()) + + registrySecret := &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, registrySecret) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the secrets") + Expect(k8sClient.Delete(ctx, onlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, offlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, registrySecret)).To(Succeed()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should fail reconciling the resource", func() { + By("Referring to a secret that doesn't exist") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "invalid_secret"} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secrets \"invalid_secret\" not found")) + + By("Referring to a secret with a key that doesn't exist") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key invalid.secret.key doesn't exist in secret online-store-secret")) + + By("Referring to a secret that contains parameter named type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named registry_type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(cassandraYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data["sql_custom_registry_key"] = nil + secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key sql in secret registry-store-secret contains invalid tag named registry_type")) + }) + + 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)) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) + 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.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + svc := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + }) + + It("should properly encode a feature_store.yaml config", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + dbParametersMap := unmarshallYamlString(sqlTypeYamlString) + copyMap := services.CopyMap(dbParametersMap) + delete(dbParametersMap, "path") + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + Path: copyMap["path"].(string), + RegistryType: services.RegistryDBPersistenceSQLConfigType, + DBParameters: dbParametersMap, + }, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineDBPersistenceSnowflakeConfigType, + DBParameters: unmarshallYamlString(snowflakeYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Type: onlineType, + DBParameters: unmarshallYamlString(cassandraYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + + // 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: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change paths and reconcile + resourceNew := resource.DeepCopy() + newOnlineSecretName := "offline-store-secret" + newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = string(services.OfflineDBPersistenceSnowflakeConfigType) + 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 online config + 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()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + + repoConfigOnline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + onlineConfig.OnlineStore.Type = services.OnlineDBPersistenceSnowflakeConfigType + onlineConfig.OnlineStore.DBParameters = unmarshallYamlString(snowflakeYamlString) + Expect(repoConfigOnline).To(Equal(onlineConfig)) + }) + }) +}) + +func unmarshallYamlString(yamlString string) map[string]interface{} { + var parameters map[string]interface{} + + err := yaml.Unmarshal([]byte(yamlString), ¶meters) + if err != nil { + fmt.Println(err) + } + return parameters +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index b24973ec86c..6f150c60d5b 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -272,6 +272,50 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } +func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + }, + } + return fsCopy +} + const resourceName = "test-resource" const namespaceName = "default" @@ -313,6 +357,10 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("s3://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("gs://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") }) + + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.online\", \"redis\", \"ikv\", \"datastore\", \"dynamodb\", \"bigtable\", \"postgres\", \"cassandra\", \"mysql\", \"hazelcast\", \"singlestore\"") + }) }) Context("When creating an invalid Offline Store", func() { @@ -321,6 +369,9 @@ var _ = Describe("FeatureStore API", func() { It("should fail when PVC persistence has absolute path", func() { attemptInvalidCreationAndAsserts(ctx, offlineStoreWithUnmanagedFileType(featurestore), "Unsupported value") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.offline\", \"bigquery\", \"redshift\", \"spark\", \"postgres\", \"feast_trino.trino.TrinoOfflineStore\", \"redis\"") + }) }) Context("When creating an invalid Registry", func() { @@ -340,6 +391,9 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForFile(featurestore), "Additional S3 settings are available only for S3 object store URIs") attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"sql\", \"snowflake.registry\"") + }) }) Context("When creating an invalid PvcConfig", func() { From 39d5c1a55b8d4230d83d15347528c0a2304f8c36 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:43:40 -0500 Subject: [PATCH 09/40] Added a description for SecretKeyName and the default behaviour Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 4bc0aa7c5e0..e5f9d7f14d4 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,9 +108,11 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOfflineStoreDBStorePersistenceTypes = []string{ @@ -149,9 +151,11 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOnlineStoreDBStorePersistenceTypes = []string{ @@ -196,9 +200,11 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidRegistryDBStorePersistenceTypes = []string{ From 84ca903efc88f297ded445f81ea58dcf141f37db Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 12:13:57 -0500 Subject: [PATCH 10/40] Added a test where the secret contains invalid type Signed-off-by: Theodor Mihalache --- .../crd/bases/feast.dev_featurestores.yaml | 18 +++++++ infra/feast-operator/dist/install.yaml | 18 +++++++ .../featurestore_controller_db_store_test.go | 47 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) 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 958c7cdddb1..2c7ce4f0501 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,6 +297,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -652,6 +655,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1025,6 +1031,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1468,6 +1477,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1829,6 +1841,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2210,6 +2225,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b5e103f9692..b83239cec0a 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,6 +305,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -660,6 +663,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1033,6 +1039,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1476,6 +1485,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1837,6 +1849,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2218,6 +2233,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 5badd4bf19b..547edbc5181 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -20,8 +20,8 @@ import ( "context" "encoding/base64" "fmt" - "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" @@ -78,7 +78,7 @@ sqlalchemy_config_kwargs: pool_pre_ping: true ` -var invalidSecretTypeYamlString = ` +var invalidSecretContainingTypeYamlString = ` type: cassandra hosts: - 192.168.1.1 @@ -96,6 +96,24 @@ read_concurrency: 100 write_concurrency: 100 ` +var invalidSecretTypeYamlString = ` +type: wrong +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var invalidSecretRegistryTypeYamlString = ` registry_type: sql path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast @@ -295,6 +313,31 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret := &corev1.Secret{} err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretContainingTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named type with invalid value") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) From 86d2a996ae22c34fa76aced948644e79651f107b Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:38:24 -0500 Subject: [PATCH 11/40] Updated the description of SecretKeyName and SecretRef parameters in the CRD Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 18 ++--- .../crd/bases/feast.dev_featurestores.yaml | 70 +++++++++---------- infra/feast-operator/dist/install.yaml | 70 +++++++++---------- 3 files changed, 77 insertions(+), 81 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index e5f9d7f14d4..bf59a1d4ba4 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,10 +108,10 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -151,10 +151,10 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -200,10 +200,10 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } 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 2c7ce4f0501..ac193e2adfd 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,14 +297,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -655,14 +654,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1031,14 +1029,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1477,14 +1475,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1841,14 +1839,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2225,14 +2223,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b83239cec0a..10fde132941 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,14 +305,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -663,14 +662,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1039,14 +1037,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1485,14 +1483,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1849,14 +1847,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2233,14 +2231,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- From cc4040fc94594f36b53333b999819003348f8d8e Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:46:23 -0500 Subject: [PATCH 12/40] Fixed error Signed-off-by: Theodor Mihalache --- .../controller/featurestore_controller_db_store_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 547edbc5181..60235fe687e 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -505,7 +505,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { From 0fd929ab8c3cd9da980fd538c15958dfb1159fc9 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 26 Nov 2024 08:48:03 -0700 Subject: [PATCH 13/40] chore: Update packages and fix lint issues (#4790) * chore: Updated testcontainers to support MilvusContainer Signed-off-by: Francisco Javier Arceo * downgraded Signed-off-by: Francisco Javier Arceo * updating libraries to downgrade singlestore to 1.7.2 Signed-off-by: Francisco Javier Arceo * fixing lint issues post package upgrades Signed-off-by: Francisco Javier Arceo * downgraded pyarrow Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo Signed-off-by: Theodor Mihalache --- sdk/python/feast/driver_test_data.py | 2 +- .../contrib/trino_offline_store/trino.py | 4 +- sdk/python/feast/registry_server.py | 7 +- .../requirements/py3.10-ci-requirements.txt | 125 +++++++++--------- .../requirements/py3.10-requirements.txt | 34 ++--- .../requirements/py3.11-ci-requirements.txt | 123 ++++++++--------- .../requirements/py3.11-requirements.txt | 32 ++--- .../requirements/py3.9-ci-requirements.txt | 119 ++++++++--------- .../requirements/py3.9-requirements.txt | 30 ++--- sdk/python/tests/data/data_creator.py | 2 +- .../online_store/test_remote_online_store.py | 5 +- .../unit/cli/test_cli_apply_duplicates.py | 20 ++- .../infra/offline_stores/test_snowflake.py | 13 +- sdk/python/tests/utils/cli_repo_creator.py | 5 +- setup.py | 6 +- 15 files changed, 276 insertions(+), 251 deletions(-) diff --git a/sdk/python/feast/driver_test_data.py b/sdk/python/feast/driver_test_data.py index 23f1f124774..d96c9c6d387 100644 --- a/sdk/python/feast/driver_test_data.py +++ b/sdk/python/feast/driver_test_data.py @@ -2,10 +2,10 @@ import itertools from datetime import timedelta, timezone from enum import Enum +from zoneinfo import ZoneInfo import numpy as np import pandas as pd -from zoneinfo import ZoneInfo from feast.infra.offline_stores.offline_utils import ( DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py index b034d4f9923..9667f4e4720 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py @@ -65,8 +65,8 @@ class JWTAuthModel(FeastConfigBaseModel): class CertificateAuthModel(FeastConfigBaseModel): - cert: FilePath = Field(default=None, alias="cert-file") - key: FilePath = Field(default=None, alias="key-file") + cert: Optional[FilePath] = Field(default=None, alias="cert-file") + key: Optional[FilePath] = Field(default=None, alias="key-file") CLASSES_BY_AUTH_TYPE = { diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 181dc79656e..c9abf62ccd7 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -792,9 +792,10 @@ def start_server( reflection.enable_server_reflection(service_names_available_for_reflection, server) if tls_cert_path and tls_key_path: - with open(tls_cert_path, "rb") as cert_file, open( - tls_key_path, "rb" - ) as key_file: + with ( + open(tls_cert_path, "rb") as cert_file, + open(tls_key_path, "rb") as key_file, + ): certificate_chain = cert_file.read() private_key = key_file.read() server_credentials = grpc.ssl_server_credentials( diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 88fd3ab7d9c..54a64f5b1ce 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -4,7 +4,7 @@ aiobotocore==2.15.2 # via feast (setup.py) aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.11 +aiohttp==3.11.7 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -40,7 +40,7 @@ async-lru==2.0.4 # via jupyterlab async-property==0.2.2 # via python-keycloak -async-timeout==4.0.3 +async-timeout==5.0.1 # via # aiohttp # redis @@ -51,13 +51,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.31.0 +azure-core==1.32.0 # via # azure-identity # azure-storage-blob azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.1 +azure-storage-blob==12.24.0 # via feast (setup.py) babel==2.16.0 # via @@ -65,9 +65,9 @@ babel==2.16.0 # sphinx beautifulsoup4==4.12.3 # via nbconvert -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) -bleach==6.1.0 +bleach==6.2.0 # via nbconvert boto3==1.35.36 # via @@ -128,7 +128,7 @@ comm==0.2.2 # ipywidgets couchbase==4.3.2 # via feast (setup.py) -coverage[toml]==7.6.4 +coverage[toml]==7.6.8 # via pytest-cov cryptography==42.0.8 # via @@ -146,21 +146,21 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.10.0 +dask[dataframe]==2024.11.2 # via # feast (setup.py) # dask-expr -dask-expr==1.1.16 +dask-expr==1.1.19 # via dask -db-dtypes==1.3.0 +db-dtypes==1.3.1 # via google-cloud-bigquery -debugpy==1.8.7 +debugpy==1.8.9 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.2 +deltalake==0.22.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -172,11 +172,11 @@ docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==1.1.2 +duckdb==1.1.3 # via ibis-framework elastic-transport==8.15.1 # via elasticsearch -elasticsearch==8.15.1 +elasticsearch==8.16.0 # via feast (setup.py) entrypoints==0.4 # via altair @@ -189,9 +189,9 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -faiss-cpu==1.9.0 +faiss-cpu==1.9.0.post1 # via feast (setup.py) -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -211,7 +211,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.22.0 +google-api-core[grpc]==2.23.0 # via # feast (setup.py) # google-cloud-bigquery @@ -220,7 +220,7 @@ google-api-core[grpc]==2.22.0 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.35.0 +google-auth==2.36.0 # via # google-api-core # google-cloud-bigquery @@ -230,11 +230,11 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.26.0 +google-cloud-bigquery[pandas]==3.27.0 # via feast (setup.py) google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) -google-cloud-bigtable==2.26.0 +google-cloud-bigtable==2.27.0 # via feast (setup.py) google-cloud-core==2.4.1 # via @@ -254,7 +254,7 @@ google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.65.0 +googleapis-common-protos[grpc]==1.66.0 # via # feast (setup.py) # google-api-core @@ -264,7 +264,7 @@ great-expectations==0.18.22 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.67.0 +grpcio==1.68.0 # via # feast (setup.py) # google-api-core @@ -306,7 +306,7 @@ hiredis==2.4.0 # via feast (setup.py) hpack==4.0.0 # via h2 -httpcore==1.0.6 +httpcore==1.0.7 # via httpx httptools==0.6.4 # via uvicorn @@ -324,7 +324,7 @@ ibis-framework[duckdb]==9.5.0 # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.1 +identify==2.6.3 # via pre-commit idna==3.10 # via @@ -355,7 +355,7 @@ isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema -jedi==0.19.1 +jedi==0.19.2 # via ipython jinja2==3.1.4 # via @@ -372,7 +372,7 @@ jmespath==1.0.1 # via # boto3 # botocore -json5==0.9.25 +json5==0.9.28 # via jupyterlab-server jsonpatch==1.33 # via great-expectations @@ -417,7 +417,7 @@ jupyter-server==2.14.2 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.5 +jupyterlab==4.2.6 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -442,7 +442,7 @@ markupsafe==3.0.2 # jinja2 # nbconvert # werkzeug -marshmallow==3.23.0 +marshmallow==3.23.1 # via great-expectations matplotlib-inline==0.1.7 # via @@ -462,7 +462,7 @@ mock==2.0.0 # via feast (setup.py) moto==4.2.14 # via feast (setup.py) -msal==1.31.0 +msal==1.31.1 # via # azure-identity # msal-extensions @@ -517,7 +517,7 @@ oauthlib==3.2.2 # via requests-oauthlib overrides==7.7.0 # via jupyter-server -packaging==24.1 +packaging==24.2 # via # build # dask @@ -589,7 +589,9 @@ prometheus-client==0.21.0 prompt-toolkit==3.0.48 # via ipython propcache==0.2.0 - # via yarl + # via + # aiohttp + # yarl proto-plus==1.25.0 # via # google-api-core @@ -621,7 +623,7 @@ psycopg[binary, pool]==3.2.3 # via feast (setup.py) psycopg-binary==3.2.3 # via psycopg -psycopg-pool==3.2.3 +psycopg-pool==3.2.4 # via psycopg ptyprocess==0.7.0 # via @@ -656,13 +658,13 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi # great-expectations # qdrant-client -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via @@ -671,13 +673,13 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.0 # via # feast (setup.py) # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.1 +pymssql==2.3.2 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) @@ -709,7 +711,7 @@ pytest-asyncio==0.23.8 # via feast (setup.py) pytest-benchmark==3.4.1 # via feast (setup.py) -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via feast (setup.py) pytest-env==1.1.3 # via feast (setup.py) @@ -763,7 +765,7 @@ pyzmq==26.2.0 # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.12.0 +qdrant-client==1.12.1 # via feast (setup.py) redis==4.6.0 # via feast (setup.py) @@ -772,7 +774,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.9.11 +regex==2024.11.6 # via # feast (setup.py) # parsimonious @@ -811,9 +813,9 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.9.3 +rich==13.9.4 # via ibis-framework -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -823,15 +825,15 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.7.1 +ruff==0.8.0 # via feast (setup.py) -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.2.0 +setuptools==75.6.0 # via # grpcio-tools # jupyterlab @@ -844,7 +846,6 @@ six==1.16.0 # via # asttokens # azure-core - # bleach # geomet # happybase # kubernetes @@ -888,7 +889,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.41.2 +starlette==0.41.3 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -900,7 +901,7 @@ terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals -testcontainers==4.4.0 +testcontainers==4.8.2 # via feast (setup.py) thriftpy2==0.5.2 # via happybase @@ -908,7 +909,7 @@ tinycss2==1.4.0 # via nbconvert toml==0.10.2 # via feast (setup.py) -tomli==2.0.2 +tomli==2.1.0 # via # build # coverage @@ -926,7 +927,7 @@ toolz==0.12.1 # dask # ibis-framework # partd -tornado==6.4.1 +tornado==6.4.2 # via # ipykernel # jupyter-client @@ -934,7 +935,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.6 +tqdm==4.67.1 # via # feast (setup.py) # great-expectations @@ -955,7 +956,7 @@ traitlets==5.14.3 # nbformat trino==0.330.0 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl @@ -963,7 +964,7 @@ types-protobuf==3.19.22 # via # feast (setup.py) # mypy-protobuf -types-pymysql==1.1.0.20240524 +types-pymysql==1.1.0.20241103 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis @@ -979,7 +980,7 @@ types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.2.0.20241025 +types-setuptools==75.6.0.20241126 # via # feast (setup.py) # types-cffi @@ -1032,7 +1033,7 @@ urllib3==2.2.3 # requests # responses # testcontainers -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -1044,11 +1045,11 @@ virtualenv==20.23.0 # via # feast (setup.py) # pre-commit -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.8.0 +webcolors==24.11.1 # via jsonschema webencodings==0.5.1 # via @@ -1058,23 +1059,23 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.1 +websockets==14.1 # via uvicorn -werkzeug==3.0.6 +werkzeug==3.1.3 # via moto -wheel==0.44.0 +wheel==0.45.1 # via # pip-tools # singlestoredb widgetsnbextension==4.0.13 # via ipywidgets -wrapt==1.16.0 +wrapt==1.17.0 # via # aiobotocore # testcontainers xmltodict==0.14.2 # via moto -yarl==1.16.0 +yarl==1.18.0 # via aiohttp -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index dd2ed6951c9..9a087b4a8eb 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -10,7 +10,7 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) certifi==2024.8.30 # via requests @@ -25,17 +25,17 @@ cloudpickle==3.1.0 # via dask colorama==0.4.6 # via feast (setup.py) -dask[dataframe]==2024.10.0 +dask[dataframe]==2024.11.2 # via # feast (setup.py) # dask-expr -dask-expr==1.1.16 +dask-expr==1.1.19 # via dask dill==0.3.9 # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fsspec==2024.10.0 # via dask @@ -74,7 +74,7 @@ numpy==1.26.4 # feast (setup.py) # dask # pandas -packaging==24.1 +packaging==24.2 # via # dask # gunicorn @@ -95,15 +95,15 @@ pyarrow==18.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via feast (setup.py) -pyjwt==2.9.0 +pyjwt==2.10.0 # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas @@ -122,7 +122,7 @@ referencing==0.35.1 # jsonschema-specifications requests==2.32.3 # via feast (setup.py) -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -132,7 +132,7 @@ sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.36 # via feast (setup.py) -starlette==0.41.2 +starlette==0.41.3 # via fastapi tabulate==0.9.0 # via feast (setup.py) @@ -140,15 +140,15 @@ tenacity==8.5.0 # via feast (setup.py) toml==0.10.2 # via feast (setup.py) -tomli==2.0.2 +tomli==2.1.0 # via mypy toolz==1.0.0 # via # dask # partd -tqdm==4.66.6 +tqdm==4.67.1 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) typing-extensions==4.12.2 # via @@ -164,7 +164,7 @@ tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -172,9 +172,9 @@ uvicorn-worker==0.2.0 # via feast (setup.py) uvloop==0.21.0 # via uvicorn -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn -websockets==13.1 +websockets==14.1 # via uvicorn -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index ba470071997..43637fd2067 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -4,7 +4,7 @@ aiobotocore==2.15.2 # via feast (setup.py) aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.11 +aiohttp==3.11.7 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -40,7 +40,7 @@ async-lru==2.0.4 # via jupyterlab async-property==0.2.2 # via python-keycloak -async-timeout==4.0.3 +async-timeout==5.0.1 # via redis atpublic==5.0 # via ibis-framework @@ -49,13 +49,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.31.0 +azure-core==1.32.0 # via # azure-identity # azure-storage-blob azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.1 +azure-storage-blob==12.24.0 # via feast (setup.py) babel==2.16.0 # via @@ -63,9 +63,9 @@ babel==2.16.0 # sphinx beautifulsoup4==4.12.3 # via nbconvert -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) -bleach==6.1.0 +bleach==6.2.0 # via nbconvert boto3==1.35.36 # via @@ -126,7 +126,7 @@ comm==0.2.2 # ipywidgets couchbase==4.3.2 # via feast (setup.py) -coverage[toml]==7.6.4 +coverage[toml]==7.6.8 # via pytest-cov cryptography==42.0.8 # via @@ -144,21 +144,21 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.10.0 +dask[dataframe]==2024.11.2 # via # feast (setup.py) # dask-expr -dask-expr==1.1.16 +dask-expr==1.1.19 # via dask -db-dtypes==1.3.0 +db-dtypes==1.3.1 # via google-cloud-bigquery -debugpy==1.8.7 +debugpy==1.8.9 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.2 +deltalake==0.22.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -170,11 +170,11 @@ docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==1.1.2 +duckdb==1.1.3 # via ibis-framework elastic-transport==8.15.1 # via elasticsearch -elasticsearch==8.15.1 +elasticsearch==8.16.0 # via feast (setup.py) entrypoints==0.4 # via altair @@ -182,9 +182,9 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -faiss-cpu==1.9.0 +faiss-cpu==1.9.0.post1 # via feast (setup.py) -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -204,7 +204,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.22.0 +google-api-core[grpc]==2.23.0 # via # feast (setup.py) # google-cloud-bigquery @@ -213,7 +213,7 @@ google-api-core[grpc]==2.22.0 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.35.0 +google-auth==2.36.0 # via # google-api-core # google-cloud-bigquery @@ -223,11 +223,11 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.26.0 +google-cloud-bigquery[pandas]==3.27.0 # via feast (setup.py) google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) -google-cloud-bigtable==2.26.0 +google-cloud-bigtable==2.27.0 # via feast (setup.py) google-cloud-core==2.4.1 # via @@ -247,7 +247,7 @@ google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.65.0 +googleapis-common-protos[grpc]==1.66.0 # via # feast (setup.py) # google-api-core @@ -257,7 +257,7 @@ great-expectations==0.18.22 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.67.0 +grpcio==1.68.0 # via # feast (setup.py) # google-api-core @@ -299,7 +299,7 @@ hiredis==2.4.0 # via feast (setup.py) hpack==4.0.0 # via h2 -httpcore==1.0.6 +httpcore==1.0.7 # via httpx httptools==0.6.4 # via uvicorn @@ -317,7 +317,7 @@ ibis-framework[duckdb]==9.5.0 # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.1 +identify==2.6.3 # via pre-commit idna==3.10 # via @@ -346,7 +346,7 @@ isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema -jedi==0.19.1 +jedi==0.19.2 # via ipython jinja2==3.1.4 # via @@ -363,7 +363,7 @@ jmespath==1.0.1 # via # boto3 # botocore -json5==0.9.25 +json5==0.9.28 # via jupyterlab-server jsonpatch==1.33 # via great-expectations @@ -408,7 +408,7 @@ jupyter-server==2.14.2 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.5 +jupyterlab==4.2.6 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -433,7 +433,7 @@ markupsafe==3.0.2 # jinja2 # nbconvert # werkzeug -marshmallow==3.23.0 +marshmallow==3.23.1 # via great-expectations matplotlib-inline==0.1.7 # via @@ -453,7 +453,7 @@ mock==2.0.0 # via feast (setup.py) moto==4.2.14 # via feast (setup.py) -msal==1.31.0 +msal==1.31.1 # via # azure-identity # msal-extensions @@ -508,7 +508,7 @@ oauthlib==3.2.2 # via requests-oauthlib overrides==7.7.0 # via jupyter-server -packaging==24.1 +packaging==24.2 # via # build # dask @@ -580,7 +580,9 @@ prometheus-client==0.21.0 prompt-toolkit==3.0.48 # via ipython propcache==0.2.0 - # via yarl + # via + # aiohttp + # yarl proto-plus==1.25.0 # via # google-api-core @@ -612,7 +614,7 @@ psycopg[binary, pool]==3.2.3 # via feast (setup.py) psycopg-binary==3.2.3 # via psycopg -psycopg-pool==3.2.3 +psycopg-pool==3.2.4 # via psycopg ptyprocess==0.7.0 # via @@ -647,13 +649,13 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi # great-expectations # qdrant-client -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via @@ -662,13 +664,13 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.0 # via # feast (setup.py) # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.1 +pymssql==2.3.2 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) @@ -700,7 +702,7 @@ pytest-asyncio==0.23.8 # via feast (setup.py) pytest-benchmark==3.4.1 # via feast (setup.py) -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via feast (setup.py) pytest-env==1.1.3 # via feast (setup.py) @@ -754,7 +756,7 @@ pyzmq==26.2.0 # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.12.0 +qdrant-client==1.12.1 # via feast (setup.py) redis==4.6.0 # via feast (setup.py) @@ -763,7 +765,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.9.11 +regex==2024.11.6 # via # feast (setup.py) # parsimonious @@ -802,9 +804,9 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.9.3 +rich==13.9.4 # via ibis-framework -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -814,15 +816,15 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.7.1 +ruff==0.8.0 # via feast (setup.py) -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.2.0 +setuptools==75.6.0 # via # grpcio-tools # jupyterlab @@ -835,7 +837,6 @@ six==1.16.0 # via # asttokens # azure-core - # bleach # geomet # happybase # kubernetes @@ -879,7 +880,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.41.2 +starlette==0.41.3 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -891,7 +892,7 @@ terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals -testcontainers==4.4.0 +testcontainers==4.8.2 # via feast (setup.py) thriftpy2==0.5.2 # via happybase @@ -907,7 +908,7 @@ toolz==0.12.1 # dask # ibis-framework # partd -tornado==6.4.1 +tornado==6.4.2 # via # ipykernel # jupyter-client @@ -915,7 +916,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.6 +tqdm==4.67.1 # via # feast (setup.py) # great-expectations @@ -936,7 +937,7 @@ traitlets==5.14.3 # nbformat trino==0.330.0 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl @@ -944,7 +945,7 @@ types-protobuf==3.19.22 # via # feast (setup.py) # mypy-protobuf -types-pymysql==1.1.0.20240524 +types-pymysql==1.1.0.20241103 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis @@ -960,7 +961,7 @@ types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.2.0.20241025 +types-setuptools==75.6.0.20241126 # via # feast (setup.py) # types-cffi @@ -1008,7 +1009,7 @@ urllib3==2.2.3 # requests # responses # testcontainers -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -1020,11 +1021,11 @@ virtualenv==20.23.0 # via # feast (setup.py) # pre-commit -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.8.0 +webcolors==24.11.1 # via jsonschema webencodings==0.5.1 # via @@ -1034,23 +1035,23 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.1 +websockets==14.1 # via uvicorn -werkzeug==3.0.6 +werkzeug==3.1.3 # via moto -wheel==0.44.0 +wheel==0.45.1 # via # pip-tools # singlestoredb widgetsnbextension==4.0.13 # via ipywidgets -wrapt==1.16.0 +wrapt==1.17.0 # via # aiobotocore # testcontainers xmltodict==0.14.2 # via moto -yarl==1.16.0 +yarl==1.18.0 # via aiohttp -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index c9833ca07b0..8f776fdc457 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -10,7 +10,7 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) certifi==2024.8.30 # via requests @@ -25,15 +25,15 @@ cloudpickle==3.1.0 # via dask colorama==0.4.6 # via feast (setup.py) -dask[dataframe]==2024.10.0 +dask[dataframe]==2024.11.2 # via # feast (setup.py) # dask-expr -dask-expr==1.1.16 +dask-expr==1.1.19 # via dask dill==0.3.9 # via feast (setup.py) -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fsspec==2024.10.0 # via dask @@ -72,7 +72,7 @@ numpy==1.26.4 # feast (setup.py) # dask # pandas -packaging==24.1 +packaging==24.2 # via # dask # gunicorn @@ -93,15 +93,15 @@ pyarrow==18.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via feast (setup.py) -pyjwt==2.9.0 +pyjwt==2.10.0 # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas @@ -120,7 +120,7 @@ referencing==0.35.1 # jsonschema-specifications requests==2.32.3 # via feast (setup.py) -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -130,7 +130,7 @@ sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.36 # via feast (setup.py) -starlette==0.41.2 +starlette==0.41.3 # via fastapi tabulate==0.9.0 # via feast (setup.py) @@ -142,9 +142,9 @@ toolz==1.0.0 # via # dask # partd -tqdm==4.66.6 +tqdm==4.67.1 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) typing-extensions==4.12.2 # via @@ -158,7 +158,7 @@ tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -166,9 +166,9 @@ uvicorn-worker==0.2.0 # via feast (setup.py) uvloop==0.21.0 # via uvicorn -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn -websockets==13.1 +websockets==14.1 # via uvicorn -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 1aaffd81738..3deb441827c 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -4,7 +4,7 @@ aiobotocore==2.15.2 # via feast (setup.py) aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.11 +aiohttp==3.11.7 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -40,7 +40,7 @@ async-lru==2.0.4 # via jupyterlab async-property==0.2.2 # via python-keycloak -async-timeout==4.0.3 +async-timeout==5.0.1 # via # aiohttp # redis @@ -51,13 +51,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.31.0 +azure-core==1.32.0 # via # azure-identity # azure-storage-blob azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.1 +azure-storage-blob==12.24.0 # via feast (setup.py) babel==2.16.0 # via @@ -67,9 +67,9 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) -bleach==6.1.0 +bleach==6.2.0 # via nbconvert boto3==1.35.36 # via @@ -130,7 +130,7 @@ comm==0.2.2 # ipywidgets couchbase==4.3.2 # via feast (setup.py) -coverage[toml]==7.6.4 +coverage[toml]==7.6.8 # via pytest-cov cryptography==42.0.8 # via @@ -154,15 +154,15 @@ dask[dataframe]==2024.8.0 # dask-expr dask-expr==1.1.10 # via dask -db-dtypes==1.3.0 +db-dtypes==1.3.1 # via google-cloud-bigquery -debugpy==1.8.7 +debugpy==1.8.9 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.2 +deltalake==0.22.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -178,7 +178,7 @@ duckdb==0.10.3 # via ibis-framework elastic-transport==8.15.1 # via elasticsearch -elasticsearch==8.15.1 +elasticsearch==8.16.0 # via feast (setup.py) entrypoints==0.4 # via altair @@ -191,9 +191,9 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -faiss-cpu==1.9.0 +faiss-cpu==1.9.0.post1 # via feast (setup.py) -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -213,7 +213,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.22.0 +google-api-core[grpc]==2.23.0 # via # feast (setup.py) # google-cloud-bigquery @@ -222,7 +222,7 @@ google-api-core[grpc]==2.22.0 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.35.0 +google-auth==2.36.0 # via # google-api-core # google-cloud-bigquery @@ -232,11 +232,11 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.26.0 +google-cloud-bigquery[pandas]==3.27.0 # via feast (setup.py) google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) -google-cloud-bigtable==2.26.0 +google-cloud-bigtable==2.27.0 # via feast (setup.py) google-cloud-core==2.4.1 # via @@ -256,7 +256,7 @@ google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.65.0 +googleapis-common-protos[grpc]==1.66.0 # via # feast (setup.py) # google-api-core @@ -266,7 +266,7 @@ great-expectations==0.18.22 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.67.0 +grpcio==1.68.0 # via # feast (setup.py) # google-api-core @@ -308,7 +308,7 @@ hiredis==2.4.0 # via feast (setup.py) hpack==4.0.0 # via h2 -httpcore==1.0.6 +httpcore==1.0.7 # via httpx httptools==0.6.4 # via uvicorn @@ -326,7 +326,7 @@ ibis-framework[duckdb]==9.0.0 # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.1 +identify==2.6.3 # via pre-commit idna==3.10 # via @@ -364,7 +364,7 @@ isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema -jedi==0.19.1 +jedi==0.19.2 # via ipython jinja2==3.1.4 # via @@ -381,7 +381,7 @@ jmespath==1.0.1 # via # boto3 # botocore -json5==0.9.25 +json5==0.9.28 # via jupyterlab-server jsonpatch==1.33 # via great-expectations @@ -426,7 +426,7 @@ jupyter-server==2.14.2 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.5 +jupyterlab==4.2.6 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -451,7 +451,7 @@ markupsafe==3.0.2 # jinja2 # nbconvert # werkzeug -marshmallow==3.23.0 +marshmallow==3.23.1 # via great-expectations matplotlib-inline==0.1.7 # via @@ -471,7 +471,7 @@ mock==2.0.0 # via feast (setup.py) moto==4.2.14 # via feast (setup.py) -msal==1.31.0 +msal==1.31.1 # via # azure-identity # msal-extensions @@ -526,7 +526,7 @@ oauthlib==3.2.2 # via requests-oauthlib overrides==7.7.0 # via jupyter-server -packaging==24.1 +packaging==24.2 # via # build # dask @@ -597,7 +597,9 @@ prometheus-client==0.21.0 prompt-toolkit==3.0.48 # via ipython propcache==0.2.0 - # via yarl + # via + # aiohttp + # yarl proto-plus==1.25.0 # via # google-api-core @@ -629,7 +631,7 @@ psycopg[binary, pool]==3.1.18 # via feast (setup.py) psycopg-binary==3.1.18 # via psycopg -psycopg-pool==3.2.3 +psycopg-pool==3.2.4 # via psycopg ptyprocess==0.7.0 # via @@ -664,13 +666,13 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi # great-expectations # qdrant-client -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via @@ -679,13 +681,13 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.0 # via # feast (setup.py) # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.1 +pymssql==2.3.2 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) @@ -717,7 +719,7 @@ pytest-asyncio==0.23.8 # via feast (setup.py) pytest-benchmark==3.4.1 # via feast (setup.py) -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via feast (setup.py) pytest-env==1.1.3 # via feast (setup.py) @@ -771,7 +773,7 @@ pyzmq==26.2.0 # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.12.0 +qdrant-client==1.12.1 # via feast (setup.py) redis==4.6.0 # via feast (setup.py) @@ -780,7 +782,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.9.11 +regex==2024.11.6 # via # feast (setup.py) # parsimonious @@ -819,9 +821,9 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.9.3 +rich==13.9.4 # via ibis-framework -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -831,15 +833,15 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.7.1 +ruff==0.8.0 # via feast (setup.py) -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 scipy==1.13.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.2.0 +setuptools==75.6.0 # via # grpcio-tools # jupyterlab @@ -852,7 +854,6 @@ six==1.16.0 # via # asttokens # azure-core - # bleach # geomet # happybase # kubernetes @@ -896,7 +897,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.41.2 +starlette==0.41.3 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -908,7 +909,7 @@ terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals -testcontainers==4.4.0 +testcontainers==4.8.2 # via feast (setup.py) thriftpy2==0.5.2 # via happybase @@ -916,7 +917,7 @@ tinycss2==1.4.0 # via nbconvert toml==0.10.2 # via feast (setup.py) -tomli==2.0.2 +tomli==2.1.0 # via # build # coverage @@ -934,7 +935,7 @@ toolz==0.12.1 # dask # ibis-framework # partd -tornado==6.4.1 +tornado==6.4.2 # via # ipykernel # jupyter-client @@ -942,7 +943,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.6 +tqdm==4.67.1 # via # feast (setup.py) # great-expectations @@ -963,7 +964,7 @@ traitlets==5.14.3 # nbformat trino==0.330.0 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl @@ -971,7 +972,7 @@ types-protobuf==3.19.22 # via # feast (setup.py) # mypy-protobuf -types-pymysql==1.1.0.20240524 +types-pymysql==1.1.0.20241103 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis @@ -987,7 +988,7 @@ types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.2.0.20241025 +types-setuptools==75.6.0.20241126 # via # feast (setup.py) # types-cffi @@ -1043,7 +1044,7 @@ urllib3==1.26.20 # responses # snowflake-connector-python # testcontainers -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -1055,11 +1056,11 @@ virtualenv==20.23.0 # via # feast (setup.py) # pre-commit -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.8.0 +webcolors==24.11.1 # via jsonschema webencodings==0.5.1 # via @@ -1069,23 +1070,23 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.1 +websockets==14.1 # via uvicorn -werkzeug==3.0.6 +werkzeug==3.1.3 # via moto -wheel==0.44.0 +wheel==0.45.1 # via # pip-tools # singlestoredb widgetsnbextension==4.0.13 # via ipywidgets -wrapt==1.16.0 +wrapt==1.17.0 # via # aiobotocore # testcontainers xmltodict==0.14.2 # via moto -yarl==1.16.0 +yarl==1.18.0 # via aiohttp -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index ec46a195c12..8c9fc036433 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -10,7 +10,7 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.3 +bigtree==0.22.3 # via feast (setup.py) certifi==2024.8.30 # via requests @@ -35,7 +35,7 @@ dill==0.3.9 # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.115.4 +fastapi==0.115.5 # via feast (setup.py) fsspec==2024.10.0 # via dask @@ -76,7 +76,7 @@ numpy==1.26.4 # feast (setup.py) # dask # pandas -packaging==24.1 +packaging==24.2 # via # dask # gunicorn @@ -97,15 +97,15 @@ pyarrow==18.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.2 +pydantic==2.10.1 # via # feast (setup.py) # fastapi -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via feast (setup.py) -pyjwt==2.9.0 +pyjwt==2.10.0 # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas @@ -124,7 +124,7 @@ referencing==0.35.1 # jsonschema-specifications requests==2.32.3 # via feast (setup.py) -rpds-py==0.20.0 +rpds-py==0.21.0 # via # jsonschema # referencing @@ -134,7 +134,7 @@ sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.36 # via feast (setup.py) -starlette==0.41.2 +starlette==0.41.3 # via fastapi tabulate==0.9.0 # via feast (setup.py) @@ -142,15 +142,15 @@ tenacity==8.5.0 # via feast (setup.py) toml==0.10.2 # via feast (setup.py) -tomli==2.0.2 +tomli==2.1.0 # via mypy toolz==1.0.0 # via # dask # partd -tqdm==4.66.6 +tqdm==4.67.1 # via feast (setup.py) -typeguard==4.4.0 +typeguard==4.4.1 # via feast (setup.py) typing-extensions==4.12.2 # via @@ -167,7 +167,7 @@ tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.32.0 +uvicorn[standard]==0.32.1 # via # feast (setup.py) # uvicorn-worker @@ -175,9 +175,9 @@ uvicorn-worker==0.2.0 # via feast (setup.py) uvloop==0.21.0 # via uvicorn -watchfiles==0.24.0 +watchfiles==1.0.0 # via uvicorn -websockets==13.1 +websockets==14.1 # via uvicorn -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata diff --git a/sdk/python/tests/data/data_creator.py b/sdk/python/tests/data/data_creator.py index 5d6cffeb9df..6b0984f799d 100644 --- a/sdk/python/tests/data/data_creator.py +++ b/sdk/python/tests/data/data_creator.py @@ -1,8 +1,8 @@ from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional +from zoneinfo import ZoneInfo import pandas as pd -from zoneinfo import ZoneInfo from feast.types import FeastType, Float32, Int32, Int64, String from feast.utils import _utc_now diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index 2519d3d9bef..10f1180d8e6 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -23,7 +23,10 @@ @pytest.mark.integration def test_remote_online_store_read(auth_config, tls_mode): - with tempfile.TemporaryDirectory() as remote_server_tmp_dir, tempfile.TemporaryDirectory() as remote_client_tmp_dir: + with ( + tempfile.TemporaryDirectory() as remote_server_tmp_dir, + tempfile.TemporaryDirectory() as remote_client_tmp_dir, + ): permissions_list = [ Permission( name="online_list_fv_perm", diff --git a/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py b/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py index e331a1cc2de..b3e350fe73c 100644 --- a/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py +++ b/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py @@ -20,7 +20,10 @@ def test_cli_apply_duplicate_data_source_names() -> None: def run_simple_apply_test(example_repo_file_name: str, expected_error: bytes): - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + with ( + tempfile.TemporaryDirectory() as repo_dir_name, + tempfile.TemporaryDirectory() as data_dir_name, + ): runner = CliRunner() # Construct an example repo in a temporary dir repo_path = Path(repo_dir_name) @@ -51,7 +54,10 @@ def test_cli_apply_imported_featureview() -> None: """ Tests that applying a feature view imported from a separate Python file is successful. """ - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + with ( + tempfile.TemporaryDirectory() as repo_dir_name, + tempfile.TemporaryDirectory() as data_dir_name, + ): runner = CliRunner() # Construct an example repo in a temporary dir repo_path = Path(repo_dir_name) @@ -97,7 +103,10 @@ def test_cli_apply_imported_featureview_with_duplication() -> None: Tests that applying feature views with duplicated names is not possible, even if one of the duplicated feature views is imported from another file. """ - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + with ( + tempfile.TemporaryDirectory() as repo_dir_name, + tempfile.TemporaryDirectory() as data_dir_name, + ): runner = CliRunner() # Construct an example repo in a temporary dir repo_path = Path(repo_dir_name) @@ -152,7 +161,10 @@ def test_cli_apply_duplicated_featureview_names_multiple_py_files() -> None: """ Test apply feature views with duplicated names from multiple py files in a feature repo using CLI """ - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + with ( + tempfile.TemporaryDirectory() as repo_dir_name, + tempfile.TemporaryDirectory() as data_dir_name, + ): runner = CliRunner() # Construct an example repo in a temporary dir repo_path = Path(repo_dir_name) diff --git a/sdk/python/tests/unit/infra/offline_stores/test_snowflake.py b/sdk/python/tests/unit/infra/offline_stores/test_snowflake.py index 6e27cba341b..59caaf0b5f2 100644 --- a/sdk/python/tests/unit/infra/offline_stores/test_snowflake.py +++ b/sdk/python/tests/unit/infra/offline_stores/test_snowflake.py @@ -48,11 +48,14 @@ def retrieval_job(request): def test_to_remote_storage(retrieval_job): stored_files = ["just a path", "maybe another"] - with patch.object( - retrieval_job, "to_snowflake", return_value=None - ) as mock_to_snowflake, patch.object( - retrieval_job, "_get_file_names_from_copy_into", return_value=stored_files - ) as mock_get_file_names_from_copy: + with ( + patch.object( + retrieval_job, "to_snowflake", return_value=None + ) as mock_to_snowflake, + patch.object( + retrieval_job, "_get_file_names_from_copy_into", return_value=stored_files + ) as mock_get_file_names_from_copy, + ): assert ( retrieval_job.to_remote_storage() == stored_files ), "should return the list of files" diff --git a/sdk/python/tests/utils/cli_repo_creator.py b/sdk/python/tests/utils/cli_repo_creator.py index 92b6dd992aa..e00104081a2 100644 --- a/sdk/python/tests/utils/cli_repo_creator.py +++ b/sdk/python/tests/utils/cli_repo_creator.py @@ -59,7 +59,10 @@ def local_repo(self, example_repo_py: str, offline_store: str): random.choice(string.ascii_lowercase + string.digits) for _ in range(10) ) - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + with ( + tempfile.TemporaryDirectory() as repo_dir_name, + tempfile.TemporaryDirectory() as data_dir_name, + ): repo_path = Path(repo_dir_name) data_path = Path(data_dir_name) diff --git a/setup.py b/setup.py index 5a6581cc853..5ee1e891bc4 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ "mmh3", "numpy>=1.22,<2", "pandas>=1.4.3,<3", - "pyarrow>=9.0.0", + "pyarrow<18.1.0", "pydantic>=2.0.0", "pygments>=2.12.0,<3", "PyYAML>=5.4.0,<7", @@ -140,7 +140,7 @@ ELASTICSEARCH_REQUIRED = ["elasticsearch>=8.13.0"] -SINGLESTORE_REQUIRED = ["singlestoredb"] +SINGLESTORE_REQUIRED = ["singlestoredb<1.8.0"] COUCHBASE_REQUIRED = ["couchbase==4.3.2"] @@ -179,7 +179,7 @@ "pytest-mock==1.10.4", "pytest-env", "Sphinx>4.0.0,<7", - "testcontainers==4.4.0", + "testcontainers==4.8.2", "python-keycloak==4.2.2", "pre-commit<3.3.2", "assertpy==1.1", From b9ffc1fe205852191cb75d7ecba114e56449d738 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:32:06 -0500 Subject: [PATCH 14/40] feat: Added feast Go operator db stores support (#4771) * Add support for db stores in feast go operator Signed-off-by: Theodor Mihalache * Added CR example for store persistence Signed-off-by: Theodor Mihalache * Fixed incorrect yaml tag in RegistryConfig struct Signed-off-by: Theodor Mihalache * Removed leftovers comments from hasAttrib function Signed-off-by: Theodor Mihalache * Added another check that object parameter type is the same as value type in hasAttrib Signed-off-by: Theodor Mihalache * Reverted latest commit Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 66 +++++- .../api/v1alpha1/zz_generated.deepcopy.go | 75 +++++++ .../crd/bases/feast.dev_featurestores.yaml | 201 ++++++++++++++++- infra/feast-operator/config/rbac/role.yaml | 7 + .../v1alpha1_featurestore_db_persistence.yaml | 15 ++ infra/feast-operator/dist/install.yaml | 208 ++++++++++++++++- .../controller/featurestore_controller.go | 1 + .../featurestore_controller_ephemeral_test.go | 2 +- .../featurestore_controller_pvc_test.go | 2 +- .../featurestore_controller_test.go | 2 +- .../controller/services/repo_config.go | 212 +++++++++++++++--- .../controller/services/repo_config_test.go | 140 ++++++++++-- .../internal/controller/services/services.go | 101 ++++++++- .../controller/services/services_types.go | 38 ++-- .../internal/controller/services/util.go | 187 ++++++++++++--- .../test/api/featurestore_types_test.go | 82 +++---- 16 files changed, 1176 insertions(+), 163 deletions(-) create mode 100644 infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 440518db747..50bf682213f 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -75,11 +75,13 @@ type OfflineStore struct { } // OfflineStorePersistence configures the persistence settings for the offline store service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." type OfflineStorePersistence struct { - FilePersistence *OfflineStoreFilePersistence `json:"file,omitempty"` + FilePersistence *OfflineStoreFilePersistence `json:"file,omitempty"` + DBPersistence *OfflineStoreDBStorePersistence `json:"store,omitempty"` } -// OfflineStorePersistence configures the file-based persistence for the offline store service +// OfflineStoreFilePersistence configures the file-based persistence for the offline store service type OfflineStoreFilePersistence struct { // +kubebuilder:validation:Enum=dask;duckdb Type string `json:"type,omitempty"` @@ -91,6 +93,24 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ "duckdb", } +// OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service +type OfflineStoreDBStorePersistence struct { + // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis + Type string `json:"type,omitempty"` + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidOfflineStoreDBStorePersistenceTypes = []string{ + "snowflake.offline", + "bigquery", + "redshift", + "spark", + "postgres", + "feast_trino.trino.TrinoOfflineStore", + "redis", +} + // OnlineStore configures the deployed online store service type OnlineStore struct { ServiceConfigs `json:",inline"` @@ -98,8 +118,10 @@ type OnlineStore struct { } // OnlineStorePersistence configures the persistence settings for the online store service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." type OnlineStorePersistence struct { - FilePersistence *OnlineStoreFilePersistence `json:"file,omitempty"` + FilePersistence *OnlineStoreFilePersistence `json:"file,omitempty"` + DBPersistence *OnlineStoreDBStorePersistence `json:"store,omitempty"` } // OnlineStoreFilePersistence configures the file-based persistence for the offline store service @@ -111,6 +133,28 @@ type OnlineStoreFilePersistence struct { PvcConfig *PvcConfig `json:"pvc,omitempty"` } +// OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service +type OnlineStoreDBStorePersistence struct { + // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore + Type string `json:"type,omitempty"` + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidOnlineStoreDBStorePersistenceTypes = []string{ + "snowflake.online", + "redis", + "ikv", + "datastore", + "dynamodb", + "bigtable", + "postgres", + "cassandra", + "mysql", + "hazelcast", + "singlestore", +} + // LocalRegistryConfig configures the deployed registry service type LocalRegistryConfig struct { ServiceConfigs `json:",inline"` @@ -119,7 +163,8 @@ type LocalRegistryConfig struct { // RegistryPersistence configures the persistence settings for the registry service type RegistryPersistence struct { - FilePersistence *RegistryFilePersistence `json:"file,omitempty"` + FilePersistence *RegistryFilePersistence `json:"file,omitempty"` + DBPersistence *RegistryDBStorePersistence `json:"store,omitempty"` } // RegistryFilePersistence configures the file-based persistence for the registry service @@ -133,6 +178,19 @@ type RegistryFilePersistence struct { S3AdditionalKwargs *map[string]string `json:"s3_additional_kwargs,omitempty"` } +// RegistryDBStorePersistence configures the DB store persistence for the registry service +type RegistryDBStorePersistence struct { + // +kubebuilder:validation:Enum=sql;snowflake.registry + Type string `json:"type,omitempty"` + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidRegistryDBStorePersistenceTypes = []string{ + "snowflake.registry", + "sql", +} + // PvcConfig defines the settings for a persistent file store based on PVCs. // We can refer to an existing PVC using the `Ref` field, or create a new one using the `Create` field. // +kubebuilder:validation:XValidation:rule="[has(self.ref), has(self.create)].exists_one(c, c)",message="One selection is required between ref and create." diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index c020af11216..196b2147005 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -236,6 +236,26 @@ func (in *OfflineStore) DeepCopy() *OfflineStore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStoreDBStorePersistence) DeepCopyInto(out *OfflineStoreDBStorePersistence) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStoreDBStorePersistence. +func (in *OfflineStoreDBStorePersistence) DeepCopy() *OfflineStoreDBStorePersistence { + if in == nil { + return nil + } + out := new(OfflineStoreDBStorePersistence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OfflineStoreFilePersistence) DeepCopyInto(out *OfflineStoreFilePersistence) { *out = *in @@ -264,6 +284,11 @@ func (in *OfflineStorePersistence) DeepCopyInto(out *OfflineStorePersistence) { *out = new(OfflineStoreFilePersistence) (*in).DeepCopyInto(*out) } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(OfflineStoreDBStorePersistence) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStorePersistence. @@ -297,6 +322,26 @@ func (in *OnlineStore) DeepCopy() *OnlineStore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStoreDBStorePersistence) DeepCopyInto(out *OnlineStoreDBStorePersistence) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStoreDBStorePersistence. +func (in *OnlineStoreDBStorePersistence) DeepCopy() *OnlineStoreDBStorePersistence { + if in == nil { + return nil + } + out := new(OnlineStoreDBStorePersistence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnlineStoreFilePersistence) DeepCopyInto(out *OnlineStoreFilePersistence) { *out = *in @@ -325,6 +370,11 @@ func (in *OnlineStorePersistence) DeepCopyInto(out *OnlineStorePersistence) { *out = new(OnlineStoreFilePersistence) (*in).DeepCopyInto(*out) } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(OnlineStoreDBStorePersistence) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStorePersistence. @@ -444,6 +494,26 @@ func (in *Registry) DeepCopy() *Registry { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryDBStorePersistence) DeepCopyInto(out *RegistryDBStorePersistence) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryDBStorePersistence. +func (in *RegistryDBStorePersistence) DeepCopy() *RegistryDBStorePersistence { + if in == nil { + return nil + } + out := new(RegistryDBStorePersistence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistryFilePersistence) DeepCopyInto(out *RegistryFilePersistence) { *out = *in @@ -483,6 +553,11 @@ func (in *RegistryPersistence) DeepCopyInto(out *RegistryPersistence) { *out = new(RegistryFilePersistence) (*in).DeepCopyInto(*out) } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(RegistryDBStorePersistence) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryPersistence. 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 99d9104b953..f1c7fca8f51 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -186,8 +186,8 @@ spec: settings for the offline store service properties: file: - description: OfflineStorePersistence configures the file-based - persistence for the offline store service + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service properties: pvc: description: |- @@ -270,7 +270,40 @@ spec: - duckdb type: string type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - feast_trino.trino.TrinoOfflineStore + - redis + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -548,7 +581,44 @@ spec: - message: Online store does not support S3 or GS buckets. rule: has(self.path) && !self.path.startsWith('s3://') && !self.path.startsWith('gs://') + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -843,6 +913,31 @@ spec: for S3 object store URIs. rule: '(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - sql + - snowflake.registry + type: string + type: object type: object resources: description: ResourceRequirements describes the compute @@ -1083,8 +1178,9 @@ spec: settings for the offline store service properties: file: - description: OfflineStorePersistence configures the - file-based persistence for the offline store service + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service properties: pvc: description: |- @@ -1167,7 +1263,41 @@ spec: - duckdb type: string type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - feast_trino.trino.TrinoOfflineStore + - redis + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1449,7 +1579,45 @@ spec: buckets. rule: has(self.path) && !self.path.startsWith('s3://') && !self.path.startsWith('gs://') + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1751,6 +1919,31 @@ spec: only for S3 object store URIs. rule: '(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - sql + - snowflake.registry + type: string + type: object type: object resources: description: ResourceRequirements describes the compute diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index 6ca20859904..a4b0acfc1cf 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -29,6 +29,13 @@ rules: - list - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list - apiGroups: - feast.dev resources: diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml new file mode 100644 index 00000000000..0540bc90ccf --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml @@ -0,0 +1,15 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: example + namespace: test +spec: + feastProject: my_project + services: + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: my-secret + secretKeyName: mykey # optional diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index f23dc8b2082..83181f53b09 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -194,8 +194,8 @@ spec: settings for the offline store service properties: file: - description: OfflineStorePersistence configures the file-based - persistence for the offline store service + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service properties: pvc: description: |- @@ -278,7 +278,40 @@ spec: - duckdb type: string type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - feast_trino.trino.TrinoOfflineStore + - redis + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -556,7 +589,44 @@ spec: - message: Online store does not support S3 or GS buckets. rule: has(self.path) && !self.path.startsWith('s3://') && !self.path.startsWith('gs://') + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -851,6 +921,31 @@ spec: for S3 object store URIs. rule: '(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - sql + - snowflake.registry + type: string + type: object type: object resources: description: ResourceRequirements describes the compute @@ -1091,8 +1186,9 @@ spec: settings for the offline store service properties: file: - description: OfflineStorePersistence configures the - file-based persistence for the offline store service + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service properties: pvc: description: |- @@ -1175,7 +1271,41 @@ spec: - duckdb type: string type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - feast_trino.trino.TrinoOfflineStore + - redis + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1457,7 +1587,45 @@ spec: buckets. rule: has(self.path) && !self.path.startsWith('s3://') && !self.path.startsWith('gs://') + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - snowflake.online + - redis + - ikv + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + type: string + type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1759,6 +1927,31 @@ spec: only for S3 object store URIs. rule: '(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + type: string + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: + enum: + - sql + - snowflake.registry + type: string + type: object type: object resources: description: ResourceRequirements describes the compute @@ -2080,6 +2273,13 @@ rules: - list - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list - apiGroups: - feast.dev resources: diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index 7c78b79d593..278ea4a78fd 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -54,6 +54,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=core,resources=secrets,verbs=get;list // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. 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 bd597f987ca..913f022022d 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -319,7 +319,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Provider: services.LocalProviderType, EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ - Type: services.OfflineDuckDbConfigType, + Type: services.OfflineFilePersistenceDuckDbConfigType, }, Registry: regRemote, } 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 33fdce38fef..f124db55a6c 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -490,7 +490,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Provider: services.LocalProviderType, EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ - Type: services.OfflineDuckDbConfigType, + Type: services.OfflineFilePersistenceDuckDbConfigType, }, Registry: regRemote, } diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 1e9bbc2f521..00a6e71c71c 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -646,7 +646,7 @@ var _ = Describe("FeatureStore Controller", func() { Provider: services.LocalProviderType, EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ - Type: services.OfflineDaskConfigType, + Type: services.OfflineFilePersistenceDaskConfigType, }, Registry: regRemote, } diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 34b62ac30ad..899a9157d9b 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -18,6 +18,7 @@ package services import ( "encoding/base64" + "fmt" "path" "strings" @@ -44,64 +45,41 @@ func (feast *FeastServices) getServiceFeatureStoreYaml(feastType FeastServiceTyp } func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) (RepoConfig, error) { - return getServiceRepoConfig(feastType, feast.FeatureStore) + return getServiceRepoConfig(feastType, feast.FeatureStore, feast.extractConfigFromSecret) } -func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1alpha1.FeatureStore) (RepoConfig, error) { +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) - isLocalRegistry := isLocalRegistry(featureStore) + isLocalReg := isLocalRegistry(featureStore) if appliedSpec.Services != nil { services := appliedSpec.Services + switch feastType { case OfflineFeastType: // Offline server has an `offline_store` section and a remote `registry` if services.OfflineStore != nil { - fileType := string(OfflineDaskConfigType) - if services.OfflineStore.Persistence != nil && - services.OfflineStore.Persistence.FilePersistence != nil && - len(services.OfflineStore.Persistence.FilePersistence.Type) > 0 { - fileType = services.OfflineStore.Persistence.FilePersistence.Type - } - - repoConfig.OfflineStore = OfflineStoreConfig{ - Type: OfflineConfigType(fileType), + err := setRepoConfigOffline(services, secretExtractionFunc, &repoConfig) + if err != nil { + return repoConfig, err } - repoConfig.OnlineStore = OnlineStoreConfig{} } case OnlineFeastType: // Online server has an `online_store` section, a remote `registry` and a remote `offline_store` if services.OnlineStore != nil { - path := DefaultOnlineStoreEphemeralPath - if services.OnlineStore.Persistence != nil && services.OnlineStore.Persistence.FilePersistence != nil { - filePersistence := services.OnlineStore.Persistence.FilePersistence - path = getActualPath(filePersistence.Path, filePersistence.PvcConfig) - } - - repoConfig.OnlineStore = OnlineStoreConfig{ - Type: OnlineSqliteConfigType, - Path: path, + err := setRepoConfigOnline(services, secretExtractionFunc, &repoConfig) + if err != nil { + return repoConfig, err } } case RegistryFeastType: // Registry server only has a `registry` section - if isLocalRegistry { - path := DefaultRegistryEphemeralPath - var s3AdditionalKwargs *map[string]string - if services != nil && services.Registry != nil && services.Registry.Local != nil && - services.Registry.Local.Persistence != nil && services.Registry.Local.Persistence.FilePersistence != nil { - filePersistence := services.Registry.Local.Persistence.FilePersistence - path = getActualPath(filePersistence.Path, filePersistence.PvcConfig) - s3AdditionalKwargs = filePersistence.S3AdditionalKwargs + if isLocalReg { + err := setRepoConfigRegistry(services, secretExtractionFunc, &repoConfig) + if err != nil { + return repoConfig, err } - repoConfig.Registry = RegistryConfig{ - RegistryType: RegistryFileConfigType, - Path: path, - S3AdditionalKwargs: s3AdditionalKwargs, - } - repoConfig.OfflineStore = OfflineStoreConfig{} - repoConfig.OnlineStore = OnlineStoreConfig{} } } } @@ -109,6 +87,121 @@ func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1al return repoConfig, nil } +func setRepoConfigRegistry(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { + repoConfig.Registry = RegistryConfig{} + repoConfig.Registry.Path = DefaultRegistryEphemeralPath + registryPersistence := services.Registry.Local.Persistence + + if registryPersistence != nil { + filePersistence := registryPersistence.FilePersistence + dbPersistence := registryPersistence.DBPersistence + + if filePersistence != nil { + repoConfig.Registry.RegistryType = RegistryFileConfigType + repoConfig.Registry.Path = getActualPath(filePersistence.Path, filePersistence.PvcConfig) + repoConfig.Registry.S3AdditionalKwargs = filePersistence.S3AdditionalKwargs + } else if dbPersistence != nil && len(dbPersistence.Type) > 0 { + repoConfig.Registry.Path = "" + repoConfig.Registry.RegistryType = RegistryConfigType(dbPersistence.Type) + secretKeyName := dbPersistence.SecretKeyName + if len(secretKeyName) == 0 { + secretKeyName = string(repoConfig.Registry.RegistryType) + } + parametersMap, err := secretExtractionFunc(dbPersistence.SecretRef.Name, secretKeyName) + if err != nil { + return err + } + + err = mergeStructWithDBParametersMap(¶metersMap, &repoConfig.Registry) + if err != nil { + return err + } + + repoConfig.Registry.DBParameters = parametersMap + } + } + + repoConfig.OfflineStore = OfflineStoreConfig{} + repoConfig.OnlineStore = OnlineStoreConfig{} + + return nil +} + +func setRepoConfigOnline(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { + repoConfig.OnlineStore = OnlineStoreConfig{} + + repoConfig.OnlineStore.Path = DefaultOnlineStoreEphemeralPath + repoConfig.OnlineStore.Type = OnlineSqliteConfigType + onlineStorePersistence := services.OnlineStore.Persistence + + if onlineStorePersistence != nil { + filePersistence := onlineStorePersistence.FilePersistence + dbPersistence := onlineStorePersistence.DBPersistence + + if filePersistence != nil { + repoConfig.OnlineStore.Path = getActualPath(filePersistence.Path, filePersistence.PvcConfig) + } else if dbPersistence != nil && len(dbPersistence.Type) > 0 { + repoConfig.OnlineStore.Path = "" + repoConfig.OnlineStore.Type = OnlineConfigType(dbPersistence.Type) + secretKeyName := dbPersistence.SecretKeyName + if len(secretKeyName) == 0 { + secretKeyName = string(repoConfig.OnlineStore.Type) + } + + parametersMap, err := secretExtractionFunc(dbPersistence.SecretRef.Name, secretKeyName) + if err != nil { + return err + } + + err = mergeStructWithDBParametersMap(¶metersMap, &repoConfig.OnlineStore) + if err != nil { + return err + } + + repoConfig.OnlineStore.DBParameters = parametersMap + } + } + + return nil +} + +func setRepoConfigOffline(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { + repoConfig.OfflineStore = OfflineStoreConfig{} + repoConfig.OfflineStore.Type = OfflineFilePersistenceDaskConfigType + offlineStorePersistence := services.OfflineStore.Persistence + + if offlineStorePersistence != nil { + dbPersistence := offlineStorePersistence.DBPersistence + filePersistence := offlineStorePersistence.FilePersistence + + if filePersistence != nil && len(filePersistence.Type) > 0 { + repoConfig.OfflineStore.Type = OfflineConfigType(filePersistence.Type) + } else if offlineStorePersistence.DBPersistence != nil && len(dbPersistence.Type) > 0 { + repoConfig.OfflineStore.Type = OfflineConfigType(dbPersistence.Type) + secretKeyName := dbPersistence.SecretKeyName + if len(secretKeyName) == 0 { + secretKeyName = string(repoConfig.OfflineStore.Type) + } + + parametersMap, err := secretExtractionFunc(dbPersistence.SecretRef.Name, secretKeyName) + if err != nil { + return err + } + + err = mergeStructWithDBParametersMap(¶metersMap, &repoConfig.OfflineStore) + if err != nil { + return err + } + + repoConfig.OfflineStore.DBParameters = parametersMap + } + } + + repoConfig.OnlineStore = OnlineStoreConfig{} + + return nil +} + func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { return yaml.Marshal(getClientRepoConfig(feast.FeatureStore)) } @@ -148,3 +241,48 @@ func getActualPath(filePath string, pvcConfig *feastdevv1alpha1.PvcConfig) strin } return path.Join(pvcConfig.MountPath, filePath) } + +func (feast *FeastServices) extractConfigFromSecret(secretRef string, secretKeyName string) (map[string]interface{}, error) { + secret, err := feast.getSecret(secretRef) + if err != nil { + return nil, err + } + parameters := map[string]interface{}{} + + val, exists := secret.Data[secretKeyName] + if !exists { + return nil, fmt.Errorf("secret key %s doesn't exist in secret %s", secretKeyName, secretRef) + } + + err = yaml.Unmarshal(val, ¶meters) + if err != nil { + return nil, fmt.Errorf("secret %s contains invalid value", secretKeyName) + } + + _, exists = parameters["type"] + if exists { + return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named type", secretKeyName, secretRef) + } + + _, exists = parameters["registry_type"] + if exists { + return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named registry_type", secretKeyName, secretRef) + } + + return parameters, nil +} + +func mergeStructWithDBParametersMap(parametersMap *map[string]interface{}, s interface{}) error { + for key, val := range *parametersMap { + hasAttribute, err := hasAttrib(s, key, val) + if err != nil { + return err + } + + if hasAttribute { + delete(*parametersMap, key) + } + } + + return nil +} 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 1868bf437b5..eaeeb4ea09f 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -17,8 +17,12 @@ limitations under the License. package services import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" ) @@ -33,19 +37,19 @@ var _ = Describe("Repo Config", func() { featureStore := minimalFeatureStore() ApplyDefaultsToStatus(featureStore) var repoConfig RepoConfig - repoConfig, err := getServiceRepoConfig(OfflineFeastType, featureStore) + repoConfig, err := getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) @@ -69,19 +73,19 @@ var _ = Describe("Repo Config", func() { }, } ApplyDefaultsToStatus(featureStore) - repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) @@ -104,25 +108,25 @@ var _ = Describe("Repo Config", func() { }, } ApplyDefaultsToStatus(featureStore) - repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - By("Having the all the services") + By("Having the all the file services") featureStore = minimalFeatureStore() featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OfflineStore: &feastdevv1alpha1.OfflineStore{ @@ -150,7 +154,7 @@ var _ = Describe("Repo Config", func() { }, } ApplyDefaultsToStatus(featureStore) - repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) expectedOfflineConfig := OfflineStoreConfig{ Type: "duckdb", @@ -159,7 +163,7 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(OnlineFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) expectedOnlineConfig := OnlineStoreConfig{ @@ -169,7 +173,7 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) - repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore) + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, emptyMockExtractConfigFromSecret) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) @@ -178,6 +182,82 @@ var _ = Describe("Repo Config", func() { Path: "/data/registry.db", } Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + + By("Having the all the db services") + featureStore = minimalFeatureStore() + featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: string(OfflineDBPersistenceSnowflakeConfigType), + SecretRef: &corev1.LocalObjectReference{ + Name: "offline-test-secret", + }, + }, + }, + }, + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: string(OnlineDBPersistenceSnowflakeConfigType), + SecretRef: &corev1.LocalObjectReference{ + Name: "online-test-secret", + }, + }, + }, + }, + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: string(RegistryDBPersistenceSnowflakeConfigType), + SecretRef: &corev1.LocalObjectReference{ + Name: "registry-test-secret", + }, + }, + }, + }, + }, + } + parameterMap := createParameterMap() + ApplyDefaultsToStatus(featureStore) + featureStore.Spec.Services.OfflineStore.Persistence.FilePersistence = nil + featureStore.Spec.Services.OnlineStore.Persistence.FilePersistence = nil + featureStore.Spec.Services.Registry.Local.Persistence.FilePersistence = nil + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, mockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) + newMap := CopyMap(parameterMap) + port := parameterMap["port"].(int) + delete(newMap, "port") + expectedOfflineConfig = OfflineStoreConfig{ + Type: OfflineDBPersistenceSnowflakeConfigType, + Port: port, + DBParameters: newMap, + } + 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.OfflineStore).To(Equal(emptyOfflineStoreConfig())) + newMap = CopyMap(parameterMap) + expectedOnlineConfig = OnlineStoreConfig{ + Type: OnlineDBPersistenceSnowflakeConfigType, + DBParameters: newMap, + } + Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) + Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig())) + + repoConfig, err = getServiceRepoConfig(RegistryFeastType, featureStore, mockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) + Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) + expectedRegistryConfig = RegistryConfig{ + RegistryType: RegistryDBPersistenceSnowflakeConfigType, + DBParameters: parameterMap, + } + Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) }) }) }) @@ -201,3 +281,37 @@ func minimalFeatureStore() *feastdevv1alpha1.FeatureStore { }, } } + +func emptyMockExtractConfigFromSecret(secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + +func mockExtractConfigFromSecret(secretRef string, secretKeyName string) (map[string]interface{}, error) { + return createParameterMap(), nil +} + +func createParameterMap() map[string]interface{} { + yamlString := ` +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var parameters map[string]interface{} + + err := yaml.Unmarshal([]byte(yamlString), ¶meters) + if err != nil { + fmt.Println(err) + } + return parameters +} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 3a079b5f496..4667c8158ad 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -43,14 +43,14 @@ func (feast *FeastServices) Deploy() error { services := feast.FeatureStore.Status.Applied.Services if services != nil { if services.OfflineStore != nil { - if services.OfflineStore.Persistence != nil && - services.OfflineStore.Persistence.FilePersistence != nil && - len(services.OfflineStore.Persistence.FilePersistence.Type) > 0 { - if err := checkOfflineStoreFilePersistenceType(services.OfflineStore.Persistence.FilePersistence.Type); err != nil { - return err - } + offlinePersistence := services.OfflineStore.Persistence + + err := feast.validateOfflineStorePersistence(offlinePersistence) + if err != nil { + return err } - if err := feast.deployFeastServiceByType(OfflineFeastType); err != nil { + + if err = feast.deployFeastServiceByType(OfflineFeastType); err != nil { return err } } else { @@ -60,7 +60,14 @@ func (feast *FeastServices) Deploy() error { } if services.OnlineStore != nil { - if err := feast.deployFeastServiceByType(OnlineFeastType); err != nil { + onlinePersistence := services.OnlineStore.Persistence + + err := feast.validateOnlineStorePersistence(onlinePersistence) + if err != nil { + return err + } + + if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { return err } } else { @@ -70,7 +77,14 @@ func (feast *FeastServices) Deploy() error { } if feast.isLocalRegistry() { - if err := feast.deployFeastServiceByType(RegistryFeastType); err != nil { + registryPersistence := services.Registry.Local.Persistence + + err := feast.validateRegistryPersistence(registryPersistence) + if err != nil { + return err + } + + if err = feast.deployFeastServiceByType(RegistryFeastType); err != nil { return err } } else { @@ -87,6 +101,75 @@ func (feast *FeastServices) Deploy() error { return nil } +func (feast *FeastServices) validateRegistryPersistence(registryPersistence *feastdevv1alpha1.RegistryPersistence) error { + if registryPersistence != nil { + dbPersistence := registryPersistence.DBPersistence + + if dbPersistence != nil && len(dbPersistence.Type) > 0 { + if err := checkRegistryDBStorePersistenceType(dbPersistence.Type); err != nil { + return err + } + + if dbPersistence.SecretRef != nil { + secretRef := dbPersistence.SecretRef.Name + if _, err := feast.getSecret(secretRef); err != nil { + return err + } + } + } + } + + return nil +} + +func (feast *FeastServices) validateOnlineStorePersistence(onlinePersistence *feastdevv1alpha1.OnlineStorePersistence) error { + if onlinePersistence != nil { + dbPersistence := onlinePersistence.DBPersistence + + if dbPersistence != nil && len(dbPersistence.Type) > 0 { + if err := checkOnlineStoreDBStorePersistenceType(dbPersistence.Type); err != nil { + return err + } + + if dbPersistence.SecretRef != nil { + secretRef := dbPersistence.SecretRef.Name + if _, err := feast.getSecret(secretRef); err != nil { + return err + } + } + } + } + + return nil +} + +func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence *feastdevv1alpha1.OfflineStorePersistence) error { + if offlinePersistence != nil { + filePersistence := offlinePersistence.FilePersistence + dbPersistence := offlinePersistence.DBPersistence + + if filePersistence != nil && len(filePersistence.Type) > 0 { + if err := checkOfflineStoreFilePersistenceType(filePersistence.Type); err != nil { + return err + } + } else if dbPersistence != nil && + len(dbPersistence.Type) > 0 { + if err := checkOfflineStoreDBStorePersistenceType(dbPersistence.Type); err != nil { + return err + } + + if dbPersistence.SecretRef != nil { + secretRef := dbPersistence.SecretRef.Name + if _, err := feast.getSecret(secretRef); err != nil { + return err + } + } + } + } + + return nil +} + func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) error { if pvcCreate, shouldCreate := shouldCreatePvc(feast.FeatureStore, feastType); shouldCreate { if err := feast.createPVC(pvcCreate, feastType); err != nil { diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index f67f8c0e465..1251a2ff9e3 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -46,15 +46,20 @@ const ( RegistryFeastType FeastServiceType = "registry" ClientFeastType FeastServiceType = "client" - OfflineRemoteConfigType OfflineConfigType = "remote" - OfflineDaskConfigType OfflineConfigType = "dask" - OfflineDuckDbConfigType OfflineConfigType = "duckdb" + OfflineRemoteConfigType OfflineConfigType = "remote" + OfflineFilePersistenceDaskConfigType OfflineConfigType = "dask" + OfflineFilePersistenceDuckDbConfigType OfflineConfigType = "duckdb" + OfflineDBPersistenceSnowflakeConfigType OfflineConfigType = "snowflake.offline" - OnlineRemoteConfigType OnlineConfigType = "remote" - OnlineSqliteConfigType OnlineConfigType = "sqlite" + OnlineRemoteConfigType OnlineConfigType = "remote" + OnlineSqliteConfigType OnlineConfigType = "sqlite" + OnlineDBPersistenceSnowflakeConfigType OnlineConfigType = "snowflake.online" + OnlineDBPersistenceCassandraConfigType OnlineConfigType = "cassandra" - RegistryRemoteConfigType RegistryConfigType = "remote" - RegistryFileConfigType RegistryConfigType = "file" + RegistryRemoteConfigType RegistryConfigType = "remote" + RegistryFileConfigType RegistryConfigType = "file" + RegistryDBPersistenceSnowflakeConfigType RegistryConfigType = "snowflake.registry" + RegistryDBPersistenceSQLConfigType RegistryConfigType = "sql" LocalProviderType FeastProviderType = "local" ) @@ -172,22 +177,25 @@ type RepoConfig struct { // OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline store. type OfflineStoreConfig struct { - Host string `yaml:"host,omitempty"` - Type OfflineConfigType `yaml:"type,omitempty"` - Port int `yaml:"port,omitempty"` + Host string `yaml:"host,omitempty"` + Type OfflineConfigType `yaml:"type,omitempty"` + Port int `yaml:"port,omitempty"` + DBParameters map[string]interface{} `yaml:",inline,omitempty"` } // OnlineStoreConfig is the configuration that relates to reading from and writing to the Feast online store. type OnlineStoreConfig struct { - Path string `yaml:"path,omitempty"` - Type OnlineConfigType `yaml:"type,omitempty"` + Path string `yaml:"path,omitempty"` + Type OnlineConfigType `yaml:"type,omitempty"` + DBParameters map[string]interface{} `yaml:",inline,omitempty"` } // RegistryConfig is the configuration that relates to reading from and writing to the Feast registry. type RegistryConfig struct { - Path string `yaml:"path,omitempty"` - RegistryType RegistryConfigType `yaml:"registry_type,omitempty"` - S3AdditionalKwargs *map[string]string `json:"s3_additional_kwargs,omitempty"` + Path string `yaml:"path,omitempty"` + RegistryType RegistryConfigType `yaml:"registry_type,omitempty"` + S3AdditionalKwargs *map[string]string `yaml:"s3_additional_kwargs,omitempty"` + DBParameters map[string]interface{} `yaml:",inline,omitempty"` } type deploymentSettings struct { diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index b68bf7916c3..5e2daee6738 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -2,12 +2,19 @@ package services import ( "fmt" + "reflect" "slices" + "strings" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ) func isLocalRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { @@ -63,55 +70,74 @@ func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { if services.Registry.Local.Persistence == nil { services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{} } - if services.Registry.Local.Persistence.FilePersistence == nil { - services.Registry.Local.Persistence.FilePersistence = &feastdevv1alpha1.RegistryFilePersistence{} - } - if len(services.Registry.Local.Persistence.FilePersistence.Path) == 0 { - services.Registry.Local.Persistence.FilePersistence.Path = defaultRegistryPath(services.Registry.Local.Persistence.FilePersistence) - } - if services.Registry.Local.Persistence.FilePersistence.PvcConfig != nil { - pvc := services.Registry.Local.Persistence.FilePersistence.PvcConfig - if pvc.Create != nil { - ensureRequestedStorage(&pvc.Create.Resources, DefaultRegistryStorageRequest) + + if services.Registry.Local.Persistence.DBPersistence == nil { + if services.Registry.Local.Persistence.FilePersistence == nil { + services.Registry.Local.Persistence.FilePersistence = &feastdevv1alpha1.RegistryFilePersistence{} + } + + if len(services.Registry.Local.Persistence.FilePersistence.Path) == 0 { + services.Registry.Local.Persistence.FilePersistence.Path = defaultRegistryPath(services.Registry.Local.Persistence.FilePersistence) + } + + if services.Registry.Local.Persistence.FilePersistence.PvcConfig != nil { + pvc := services.Registry.Local.Persistence.FilePersistence.PvcConfig + if pvc.Create != nil { + ensureRequestedStorage(&pvc.Create.Resources, DefaultRegistryStorageRequest) + } } } + setServiceDefaultConfigs(&services.Registry.Local.ServiceConfigs.DefaultConfigs) } if services.OfflineStore != nil { - setServiceDefaultConfigs(&services.OfflineStore.ServiceConfigs.DefaultConfigs) if services.OfflineStore.Persistence == nil { services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{} } - if services.OfflineStore.Persistence.FilePersistence == nil { - services.OfflineStore.Persistence.FilePersistence = &feastdevv1alpha1.OfflineStoreFilePersistence{} - } - if len(services.OfflineStore.Persistence.FilePersistence.Type) == 0 { - services.OfflineStore.Persistence.FilePersistence.Type = string(OfflineDaskConfigType) - } - if services.OfflineStore.Persistence.FilePersistence.PvcConfig != nil { - pvc := services.OfflineStore.Persistence.FilePersistence.PvcConfig - if pvc.Create != nil { - ensureRequestedStorage(&pvc.Create.Resources, DefaultOfflineStorageRequest) + + if services.OfflineStore.Persistence.DBPersistence == nil { + if services.OfflineStore.Persistence.FilePersistence == nil { + services.OfflineStore.Persistence.FilePersistence = &feastdevv1alpha1.OfflineStoreFilePersistence{} + } + + if len(services.OfflineStore.Persistence.FilePersistence.Type) == 0 { + services.OfflineStore.Persistence.FilePersistence.Type = string(OfflineFilePersistenceDaskConfigType) + } + + if services.OfflineStore.Persistence.FilePersistence.PvcConfig != nil { + pvc := services.OfflineStore.Persistence.FilePersistence.PvcConfig + if pvc.Create != nil { + ensureRequestedStorage(&pvc.Create.Resources, DefaultOfflineStorageRequest) + } } } + + setServiceDefaultConfigs(&services.OfflineStore.ServiceConfigs.DefaultConfigs) } + if services.OnlineStore != nil { - setServiceDefaultConfigs(&services.OnlineStore.ServiceConfigs.DefaultConfigs) if services.OnlineStore.Persistence == nil { services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{} } - if services.OnlineStore.Persistence.FilePersistence == nil { - services.OnlineStore.Persistence.FilePersistence = &feastdevv1alpha1.OnlineStoreFilePersistence{} - } - if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { - services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(services.OnlineStore.Persistence.FilePersistence) - } - if services.OnlineStore.Persistence.FilePersistence.PvcConfig != nil { - pvc := services.OnlineStore.Persistence.FilePersistence.PvcConfig - if pvc.Create != nil { - ensureRequestedStorage(&pvc.Create.Resources, DefaultOnlineStorageRequest) + + if services.OnlineStore.Persistence.DBPersistence == nil { + if services.OnlineStore.Persistence.FilePersistence == nil { + services.OnlineStore.Persistence.FilePersistence = &feastdevv1alpha1.OnlineStoreFilePersistence{} + } + + if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { + services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(services.OnlineStore.Persistence.FilePersistence) + } + + if services.OnlineStore.Persistence.FilePersistence.PvcConfig != nil { + pvc := services.OnlineStore.Persistence.FilePersistence.PvcConfig + if pvc.Create != nil { + ensureRequestedStorage(&pvc.Create.Resources, DefaultOnlineStorageRequest) + } } } + + setServiceDefaultConfigs(&services.OnlineStore.ServiceConfigs.DefaultConfigs) } // overwrite status.applied with every reconcile applied.DeepCopyInto(&cr.Status.Applied) @@ -127,7 +153,7 @@ func checkOfflineStoreFilePersistenceType(value string) error { if slices.Contains(feastdevv1alpha1.ValidOfflineStoreFilePersistenceTypes, value) { return nil } - return fmt.Errorf("invalid file type %s for offline store", value) + return fmt.Errorf("invalid file type %s for offline store", value) } func ensureRequestedStorage(resources *v1.VolumeResourceRequirements, requestedStorage string) { @@ -145,9 +171,104 @@ func defaultOnlineStorePath(persistence *feastdevv1alpha1.OnlineStoreFilePersist } return DefaultOnlineStorePvcPath } + func defaultRegistryPath(persistence *feastdevv1alpha1.RegistryFilePersistence) string { if persistence.PvcConfig == nil { return DefaultRegistryEphemeralPath } return DefaultRegistryPvcPath } + +func checkOfflineStoreDBStorePersistenceType(value string) error { + if slices.Contains(feastdevv1alpha1.ValidOfflineStoreDBStorePersistenceTypes, value) { + return nil + } + return fmt.Errorf("invalid DB store type %s for offline store", value) +} + +func checkOnlineStoreDBStorePersistenceType(value string) error { + if slices.Contains(feastdevv1alpha1.ValidOnlineStoreDBStorePersistenceTypes, value) { + return nil + } + return fmt.Errorf("invalid DB store type %s for online store", value) +} + +func checkRegistryDBStorePersistenceType(value string) error { + if slices.Contains(feastdevv1alpha1.ValidRegistryDBStorePersistenceTypes, value) { + return nil + } + return fmt.Errorf("invalid DB store type %s for registry", value) +} + +func (feast *FeastServices) getSecret(secretRef string) (*corev1.Secret, error) { + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretRef, Namespace: feast.FeatureStore.Namespace}} + objectKey := client.ObjectKeyFromObject(secret) + if err := feast.Client.Get(feast.Context, objectKey, secret); err != nil { + if apierrors.IsNotFound(err) || err != nil { + logger := log.FromContext(feast.Context) + logger.Error(err, "invalid secret "+secretRef+" for offline store") + + return nil, err + } + } + + return secret, nil +} + +// Function to check if a struct has a specific field or field tag and sets the value in the field if empty +func hasAttrib(s interface{}, fieldName string, value interface{}) (bool, error) { + val := reflect.ValueOf(s) + + // Check that the object is a pointer so we can modify it + if val.Kind() != reflect.Ptr || val.IsNil() { + return false, fmt.Errorf("expected a pointer to struct, got %v", val.Kind()) + } + + val = val.Elem() + + // Loop through the fields and check the tag + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := val.Type().Field(i) + + tagVal := fieldType.Tag.Get("yaml") + + // Remove other metadata if exists + commaIndex := strings.Index(tagVal, ",") + + if commaIndex != -1 { + tagVal = tagVal[:commaIndex] + } + + // Check if the field name or the tag value matches the one we're looking for + if strings.EqualFold(fieldType.Name, fieldName) || strings.EqualFold(tagVal, fieldName) { + + // Ensure the field is settable + if !field.CanSet() { + return false, fmt.Errorf("cannot set field %s", fieldName) + } + + // Check if the field is empty (zero value) + if field.IsZero() { + // Set the field value only if it's empty + field.Set(reflect.ValueOf(value)) + } + + return true, nil + } + } + + return false, nil +} + +func CopyMap(original map[string]interface{}) map[string]interface{} { + // Create a new map to store the copy + newCopy := make(map[string]interface{}) + + // Loop through the original map and copy each key-value pair + for key, value := range original { + newCopy[key] = value + } + + return newCopy +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index ac690bb7c2c..16af55b03be 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -40,8 +40,8 @@ func attemptInvalidCreationAndAsserts(ctx context.Context, featurestore *feastde } func onlineStoreWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{ Persistence: &feastdevv1alpha1.OnlineStorePersistence{ FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ @@ -51,11 +51,11 @@ func onlineStoreWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureSto }, }, } - return copy + return fsCopy } func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{ Persistence: &feastdevv1alpha1.OnlineStorePersistence{ FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ @@ -64,12 +64,12 @@ func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feat }, }, } - return copy + return fsCopy } func onlineStoreWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{ Persistence: &feastdevv1alpha1.OnlineStorePersistence{ FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ @@ -82,12 +82,12 @@ func onlineStoreWithObjectStoreBucketForPvc(path string, featureStore *feastdevv }, }, } - return copy + return fsCopy } func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OfflineStore: &feastdevv1alpha1.OfflineStore{ Persistence: &feastdevv1alpha1.OfflineStorePersistence{ FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ @@ -96,12 +96,12 @@ func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1alpha1.FeatureSto }, }, } - return copy + return fsCopy } func registryWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ Registry: &feastdevv1alpha1.Registry{ Local: &feastdevv1alpha1.LocalRegistryConfig{ Persistence: &feastdevv1alpha1.RegistryPersistence{ @@ -112,11 +112,11 @@ func registryWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureStore) }, }, } - return copy + return fsCopy } func registryWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ Registry: &feastdevv1alpha1.Registry{ Local: &feastdevv1alpha1.LocalRegistryConfig{ Persistence: &feastdevv1alpha1.RegistryPersistence{ @@ -127,11 +127,11 @@ func registryWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feature }, }, } - return copy + return fsCopy } func registryWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ Registry: &feastdevv1alpha1.Registry{ Local: &feastdevv1alpha1.LocalRegistryConfig{ Persistence: &feastdevv1alpha1.RegistryPersistence{ @@ -146,11 +146,11 @@ func registryWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1al }, }, } - return copy + return fsCopy } func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ Registry: &feastdevv1alpha1.Registry{ Local: &feastdevv1alpha1.LocalRegistryConfig{ Persistence: &feastdevv1alpha1.RegistryPersistence{ @@ -162,11 +162,11 @@ func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1alpha1.Feat }, }, } - return copy + return fsCopy } func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ Registry: &feastdevv1alpha1.Registry{ Local: &feastdevv1alpha1.LocalRegistryConfig{ Persistence: &feastdevv1alpha1.RegistryPersistence{ @@ -178,12 +178,12 @@ func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1alpha1. }, }, } - return copy + return fsCopy } func pvcConfigWithNeitherRefNorCreate(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OfflineStore: &feastdevv1alpha1.OfflineStore{ Persistence: &feastdevv1alpha1.OfflineStorePersistence{ FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ @@ -192,11 +192,11 @@ func pvcConfigWithNeitherRefNorCreate(featureStore *feastdevv1alpha1.FeatureStor }, }, } - return copy + return fsCopy } func pvcConfigWithBothRefAndCreate(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OfflineStore: &feastdevv1alpha1.OfflineStore{ Persistence: &feastdevv1alpha1.OfflineStorePersistence{ FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ @@ -210,12 +210,12 @@ func pvcConfigWithBothRefAndCreate(featureStore *feastdevv1alpha1.FeatureStore) }, }, } - return copy + return fsCopy } func pvcConfigWithNoResources(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := featureStore.DeepCopy() - copy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ OfflineStore: &feastdevv1alpha1.OfflineStore{ Persistence: &feastdevv1alpha1.OfflineStorePersistence{ FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ @@ -249,27 +249,27 @@ func pvcConfigWithNoResources(featureStore *feastdevv1alpha1.FeatureStore) *feas }, }, } - return copy + return fsCopy } func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - copy := pvcConfigWithNoResources(featureStore) - copy.Spec.Services.OfflineStore.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ + fsCopy := pvcConfigWithNoResources(featureStore) + fsCopy.Spec.Services.OfflineStore.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceStorage: resource.MustParse("10Gi"), }, } - copy.Spec.Services.OnlineStore.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ + fsCopy.Spec.Services.OnlineStore.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceStorage: resource.MustParse("1Gi"), }, } - copy.Spec.Services.Registry.Local.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ + fsCopy.Spec.Services.Registry.Local.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceStorage: resource.MustParse("500Mi"), }, } - return copy + return fsCopy } const resourceName = "test-resource" From 7d8292beb0d892d74d8575c9745db15964ef028e Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 27 Nov 2024 06:34:05 -0700 Subject: [PATCH 15/40] chore: Rename test_validation to be more explicit about what it is doing (#4794) Signed-off-by: Francisco Javier Arceo Signed-off-by: Theodor Mihalache --- .../offline_store/{test_validation.py => test_dqm_validation.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sdk/python/tests/integration/offline_store/{test_validation.py => test_dqm_validation.py} (100%) diff --git a/sdk/python/tests/integration/offline_store/test_validation.py b/sdk/python/tests/integration/offline_store/test_dqm_validation.py similarity index 100% rename from sdk/python/tests/integration/offline_store/test_validation.py rename to sdk/python/tests/integration/offline_store/test_dqm_validation.py From f72b2f83a60c0b58fa99706ce64a1c75685e4011 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Wed, 27 Nov 2024 15:01:35 -0600 Subject: [PATCH 16/40] fix: Fix db store types in Operator CRD (#4798) fix dbstore types Signed-off-by: Tommy Hughes Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 18 +++++++------- .../api/v1alpha1/zz_generated.deepcopy.go | 24 +++++-------------- .../crd/bases/feast.dev_featurestores.yaml | 18 ++++++++++++++ infra/feast-operator/dist/install.yaml | 18 ++++++++++++++ .../controller/services/repo_config_test.go | 6 ++--- .../internal/controller/services/services.go | 6 ++--- 6 files changed, 57 insertions(+), 33 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 50bf682213f..5907ca87206 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -96,9 +96,9 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type,omitempty"` - SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOfflineStoreDBStorePersistenceTypes = []string{ @@ -136,9 +136,9 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type,omitempty"` - SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOnlineStoreDBStorePersistenceTypes = []string{ @@ -181,9 +181,9 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type,omitempty"` - SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidRegistryDBStorePersistenceTypes = []string{ diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 196b2147005..be0a4201efd 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -239,11 +239,7 @@ func (in *OfflineStore) DeepCopy() *OfflineStore { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OfflineStoreDBStorePersistence) DeepCopyInto(out *OfflineStoreDBStorePersistence) { *out = *in - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.LocalObjectReference) - **out = **in - } + out.SecretRef = in.SecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStoreDBStorePersistence. @@ -287,7 +283,7 @@ func (in *OfflineStorePersistence) DeepCopyInto(out *OfflineStorePersistence) { if in.DBPersistence != nil { in, out := &in.DBPersistence, &out.DBPersistence *out = new(OfflineStoreDBStorePersistence) - (*in).DeepCopyInto(*out) + **out = **in } } @@ -325,11 +321,7 @@ func (in *OnlineStore) DeepCopy() *OnlineStore { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnlineStoreDBStorePersistence) DeepCopyInto(out *OnlineStoreDBStorePersistence) { *out = *in - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.LocalObjectReference) - **out = **in - } + out.SecretRef = in.SecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStoreDBStorePersistence. @@ -373,7 +365,7 @@ func (in *OnlineStorePersistence) DeepCopyInto(out *OnlineStorePersistence) { if in.DBPersistence != nil { in, out := &in.DBPersistence, &out.DBPersistence *out = new(OnlineStoreDBStorePersistence) - (*in).DeepCopyInto(*out) + **out = **in } } @@ -497,11 +489,7 @@ func (in *Registry) DeepCopy() *Registry { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistryDBStorePersistence) DeepCopyInto(out *RegistryDBStorePersistence) { *out = *in - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.LocalObjectReference) - **out = **in - } + out.SecretRef = in.SecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryDBStorePersistence. @@ -556,7 +544,7 @@ func (in *RegistryPersistence) DeepCopyInto(out *RegistryPersistence) { if in.DBPersistence != nil { in, out := &in.DBPersistence, &out.DBPersistence *out = new(RegistryDBStorePersistence) - (*in).DeepCopyInto(*out) + **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 f1c7fca8f51..e4f1357cbaf 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -299,6 +299,9 @@ spec: - feast_trino.trino.TrinoOfflineStore - redis type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -614,6 +617,9 @@ spec: - hazelcast - singlestore type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -937,6 +943,9 @@ spec: - sql - snowflake.registry type: string + required: + - secretRef + - type type: object type: object resources: @@ -1292,6 +1301,9 @@ spec: - feast_trino.trino.TrinoOfflineStore - redis type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -1612,6 +1624,9 @@ spec: - hazelcast - singlestore type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -1943,6 +1958,9 @@ spec: - sql - snowflake.registry type: string + required: + - secretRef + - type type: object type: object resources: diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 83181f53b09..2b02676baf6 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -307,6 +307,9 @@ spec: - feast_trino.trino.TrinoOfflineStore - redis type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -622,6 +625,9 @@ spec: - hazelcast - singlestore type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -945,6 +951,9 @@ spec: - sql - snowflake.registry type: string + required: + - secretRef + - type type: object type: object resources: @@ -1300,6 +1309,9 @@ spec: - feast_trino.trino.TrinoOfflineStore - redis type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -1620,6 +1632,9 @@ spec: - hazelcast - singlestore type: string + required: + - secretRef + - type type: object type: object x-kubernetes-validations: @@ -1951,6 +1966,9 @@ spec: - sql - snowflake.registry type: string + required: + - secretRef + - type type: object type: object resources: 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 eaeeb4ea09f..cb3c4dabfe9 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -190,7 +190,7 @@ var _ = Describe("Repo Config", func() { Persistence: &feastdevv1alpha1.OfflineStorePersistence{ DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ Type: string(OfflineDBPersistenceSnowflakeConfigType), - SecretRef: &corev1.LocalObjectReference{ + SecretRef: corev1.LocalObjectReference{ Name: "offline-test-secret", }, }, @@ -200,7 +200,7 @@ var _ = Describe("Repo Config", func() { Persistence: &feastdevv1alpha1.OnlineStorePersistence{ DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ Type: string(OnlineDBPersistenceSnowflakeConfigType), - SecretRef: &corev1.LocalObjectReference{ + SecretRef: corev1.LocalObjectReference{ Name: "online-test-secret", }, }, @@ -211,7 +211,7 @@ var _ = Describe("Repo Config", func() { Persistence: &feastdevv1alpha1.RegistryPersistence{ DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ Type: string(RegistryDBPersistenceSnowflakeConfigType), - SecretRef: &corev1.LocalObjectReference{ + SecretRef: corev1.LocalObjectReference{ Name: "registry-test-secret", }, }, diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 4667c8158ad..7974ee997bd 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -110,7 +110,7 @@ func (feast *FeastServices) validateRegistryPersistence(registryPersistence *fea return err } - if dbPersistence.SecretRef != nil { + if len(dbPersistence.SecretRef.Name) > 0 { secretRef := dbPersistence.SecretRef.Name if _, err := feast.getSecret(secretRef); err != nil { return err @@ -131,7 +131,7 @@ func (feast *FeastServices) validateOnlineStorePersistence(onlinePersistence *fe return err } - if dbPersistence.SecretRef != nil { + if len(dbPersistence.SecretRef.Name) > 0 { secretRef := dbPersistence.SecretRef.Name if _, err := feast.getSecret(secretRef); err != nil { return err @@ -158,7 +158,7 @@ func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence * return err } - if dbPersistence.SecretRef != nil { + if len(dbPersistence.SecretRef.Name) > 0 { secretRef := dbPersistence.SecretRef.Name if _, err := feast.getSecret(secretRef); err != nil { return err From 7f71f7a01fe42d52e670b22090a721bb2fd6feee Mon Sep 17 00:00:00 2001 From: Matt Green Date: Wed, 27 Nov 2024 13:03:28 -0800 Subject: [PATCH 17/40] fix: Add adapters for sqlite datetime conversion (#4797) * fix: sqlite adapter deprecation warning Signed-off-by: Matt Green * fix: remove duplicate folder definition in ruff config Signed-off-by: Matt Green * update minimum ruff version and format files Signed-off-by: Matt Green --------- Signed-off-by: Matt Green Signed-off-by: Theodor Mihalache --- .../feast/infra/online_stores/sqlite.py | 42 ++++++++++++++++++- sdk/python/pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/sqlite.py b/sdk/python/feast/infra/online_stores/sqlite.py index 1b79b1a94ba..e2eeb038d00 100644 --- a/sdk/python/feast/infra/online_stores/sqlite.py +++ b/sdk/python/feast/infra/online_stores/sqlite.py @@ -17,7 +17,7 @@ import sqlite3 import struct import sys -from datetime import datetime +from datetime import date, datetime from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union @@ -39,6 +39,46 @@ from feast.utils import _build_retrieve_online_document_record, to_naive_utc +def adapt_date_iso(val: date): + """Adapt datetime.date to ISO 8601 date.""" + return val.isoformat() + + +def adapt_datetime_iso(val: datetime): + """Adapt datetime.datetime to timezone-naive ISO 8601 date.""" + return val.isoformat() + + +def adapt_datetime_epoch(val: datetime): + """Adapt datetime.datetime to Unix timestamp.""" + return int(val.timestamp()) + + +sqlite3.register_adapter(date, adapt_date_iso) +sqlite3.register_adapter(datetime, adapt_datetime_iso) +sqlite3.register_adapter(datetime, adapt_datetime_epoch) + + +def convert_date(val: bytes): + """Convert ISO 8601 date to datetime.date object.""" + return date.fromisoformat(val.decode()) + + +def convert_datetime(val: bytes): + """Convert ISO 8601 datetime to datetime.datetime object.""" + return datetime.fromisoformat(val.decode()) + + +def convert_timestamp(val: bytes): + """Convert Unix epoch timestamp to datetime.datetime object.""" + return datetime.fromtimestamp(int(val)) + + +sqlite3.register_converter("date", convert_date) +sqlite3.register_converter("datetime", convert_datetime) +sqlite3.register_converter("timestamp", convert_timestamp) + + class SqliteOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): """Online store config for local (SQLite-based) store""" diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 10ad007fa90..8a1c5b70c3b 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -6,7 +6,7 @@ select = ["E","F","W","I"] ignore = ["E203", "E266", "E501", "E721"] [tool.ruff.lint.isort] -known-first-party = ["feast", "feast", "feast_serving_server", "feast_core_server"] +known-first-party = ["feast", "feast_serving_server", "feast_core_server"] default-section = "third-party" [tool.mypy] diff --git a/setup.py b/setup.py index 5ee1e891bc4..05f57f60539 100644 --- a/setup.py +++ b/setup.py @@ -155,7 +155,7 @@ "build", "virtualenv==20.23.0", "cryptography>=35.0,<43", - "ruff>=0.3.3", + "ruff>=0.8.0", "mypy-protobuf>=3.1", "grpcio-tools>=1.56.2,<2", "grpcio-testing>=1.56.2,<2", From 8a847f44fab28204e559cd5126f7c17a6f9c991f Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:22:43 +0200 Subject: [PATCH 18/40] chore: Upgrade TypeScript to latest version 5.7.2 in /ui (#4788) Lock the TypeScript minor version by using `~` instead of `^` in the version specifier, see https://github.com/fastify/fastify-type-provider-typebox/pull/169#issuecomment-2421682145. After upgrading, the TypeScript compiler complained that the `long` package has no default export. Related fixes: - Add the latest `long` version as a direct dependency since our code uses it directly; no compilation errors with the latest version - The `long` package exports a `Long` class, so use a capital first letter in the import name to match that Signed-off-by: Harri Lehtola Signed-off-by: Theodor Mihalache --- ui/package.json | 3 ++- ui/src/utils/timestamp.ts | 4 ++-- ui/yarn.lock | 16 ++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ui/package.json b/ui/package.json index 7f583e29153..fab57d708f9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -28,6 +28,7 @@ "@emotion/css": "^11.13.0", "@emotion/react": "^11.13.3", "inter-ui": "^3.19.3", + "long": "^5.2.3", "moment": "^2.29.1", "protobufjs": "^7.1.1", "query-string": "^7.1.1", @@ -119,7 +120,7 @@ "source-map-loader": "^3.0.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.2.5", - "typescript": "^4.9.5", + "typescript": "~5.7.2", "webpack": "^5.64.4", "webpack-dev-server": "^4.6.0", "webpack-manifest-plugin": "^4.0.2", diff --git a/ui/src/utils/timestamp.ts b/ui/src/utils/timestamp.ts index 869d24870f0..4432545457c 100644 --- a/ui/src/utils/timestamp.ts +++ b/ui/src/utils/timestamp.ts @@ -1,9 +1,9 @@ -import long from 'long'; +import Long from 'long'; import { google } from '../protos'; export function toDate(ts: google.protobuf.ITimestamp) { var seconds: number; - if (ts.seconds instanceof long) { + if (ts.seconds instanceof Long) { seconds = ts.seconds.low } else { seconds = ts.seconds!; diff --git a/ui/yarn.lock b/ui/yarn.lock index 90ba33269eb..0057595f391 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8266,10 +8266,10 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -long@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" - integrity sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w== +long@^5.0.0, long@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" @@ -11474,10 +11474,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@~5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" From c928c536e30ce5c6673581dabcfab819925ccb11 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:14:59 +0100 Subject: [PATCH 19/40] feat: RBAC Authorization in Feast Operator (#4786) * Initial commit Signed-off-by: Daniele Martinoli * refactoring types with FeastHandler Signed-off-by: Daniele Martinoli * no private image Signed-off-by: Daniele Martinoli * removed log-level Signed-off-by: Daniele Martinoli * no empty list for default Role Signed-off-by: Daniele Martinoli * removed nameLabelKey, using serices.NameLabelKey Signed-off-by: Daniele Martinoli * improved CRD comments and using IsLocalRegistry Signed-off-by: Daniele Martinoli * fixing generated code Signed-off-by: Daniele Martinoli * renamed auth condition and types Signed-off-by: Daniele Martinoli * post rebase fixes Signed-off-by: Daniele Martinoli * more renamings Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 53 +- .../api/v1alpha1/zz_generated.deepcopy.go | 45 ++ .../crd/bases/feast.dev_featurestores.yaml | 44 ++ infra/feast-operator/config/rbac/role.yaml | 11 + ...ha1_featurestore_all_services_default.yaml | 14 + ...v1alpha1_featurestore_kubernetes_auth.yaml | 25 + infra/feast-operator/dist/install.yaml | 55 ++ .../internal/controller/authz/authz.go | 220 +++++++ .../internal/controller/authz/authz_types.go | 28 + .../controller/featurestore_controller.go | 50 +- .../featurestore_controller_ephemeral_test.go | 37 +- ...restore_controller_kubernetes_auth_test.go | 566 ++++++++++++++++++ ...eaturestore_controller_objectstore_test.go | 31 +- .../featurestore_controller_pvc_test.go | 41 +- .../featurestore_controller_test.go | 115 ++-- .../internal/controller/handler/handler.go | 28 + .../controller/handler/handler_types.go | 20 + .../internal/controller/services/client.go | 8 +- .../controller/services/repo_config.go | 20 +- .../controller/services/repo_config_test.go | 69 +++ .../internal/controller/services/services.go | 130 ++-- .../controller/services/services_types.go | 23 +- .../internal/controller/services/util.go | 15 +- .../test/api/featurestore_types_test.go | 6 + 24 files changed, 1461 insertions(+), 193 deletions(-) create mode 100644 infra/feast-operator/config/samples/v1alpha1_featurestore_all_services_default.yaml create mode 100644 infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml create mode 100644 infra/feast-operator/internal/controller/authz/authz.go create mode 100644 infra/feast-operator/internal/controller/authz/authz_types.go create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go create mode 100644 infra/feast-operator/internal/controller/handler/handler.go create mode 100644 infra/feast-operator/internal/controller/handler/handler_types.go 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{})) + }) }) }) From a1bc04be2701b74102812a3ba89585ae224d9d7e Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Mon, 2 Dec 2024 14:18:46 -0600 Subject: [PATCH 20/40] feat: Add TLS support to the Operator (#4796) * add tls support to the operator Signed-off-by: Tommy Hughes * operator tls review fix: if statement Signed-off-by: Tommy Hughes * rebase fixes Signed-off-by: Tommy Hughes * authz rbac fixes Signed-off-by: Tommy Hughes --------- Signed-off-by: Tommy Hughes Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 55 +- .../api/v1alpha1/zz_generated.deepcopy.go | 98 ++++ infra/feast-operator/cmd/main.go | 3 + .../crd/bases/feast.dev_featurestores.yaml | 296 +++++++++++ infra/feast-operator/config/rbac/role.yaml | 1 + infra/feast-operator/dist/install.yaml | 297 +++++++++++ .../internal/controller/authz/authz.go | 47 +- .../controller/featurestore_controller.go | 40 +- .../featurestore_controller_ephemeral_test.go | 6 +- ...eaturestore_controller_objectstore_test.go | 2 +- .../featurestore_controller_pvc_test.go | 2 +- .../featurestore_controller_test.go | 12 +- .../featurestore_controller_tls_test.go | 489 ++++++++++++++++++ .../internal/controller/services/client.go | 31 ++ .../controller/services/repo_config.go | 38 +- .../controller/services/repo_config_test.go | 10 + .../internal/controller/services/services.go | 365 ++++++++----- .../controller/services/services_types.go | 33 +- .../controller/services/suite_test.go | 4 + .../internal/controller/services/tls.go | 245 +++++++++ .../internal/controller/services/tls_test.go | 276 ++++++++++ .../internal/controller/services/util.go | 42 +- .../test/api/featurestore_types_test.go | 3 +- 23 files changed, 2190 insertions(+), 205 deletions(-) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_tls_test.go create mode 100644 infra/feast-operator/internal/controller/services/tls.go create mode 100644 infra/feast-operator/internal/controller/services/tls_test.go diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 471db0bfaab..4bc0aa7c5e0 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -33,7 +33,7 @@ const ( OnlineStoreReadyType = "OnlineStore" RegistryReadyType = "Registry" ReadyType = "FeatureStore" - AuthorizationReadyType = "AuthorizationReadyType" + AuthorizationReadyType = "Authorization" // Feast condition reasons: ReadyReason = "Ready" @@ -76,6 +76,14 @@ type FeatureStoreServices struct { type OfflineStore struct { ServiceConfigs `json:",inline"` Persistence *OfflineStorePersistence `json:"persistence,omitempty"` + TLS *OfflineTlsConfigs `json:"tls,omitempty"` +} + +// OfflineTlsConfigs configures server TLS for the offline feast service. in an openshift cluster, this is configured by default using service serving certificates. +type OfflineTlsConfigs struct { + TlsConfigs `json:",inline"` + // verify the client TLS certificate. + VerifyClient *bool `json:"verifyClient,omitempty"` } // OfflineStorePersistence configures the persistence settings for the offline store service @@ -119,6 +127,7 @@ var ValidOfflineStoreDBStorePersistenceTypes = []string{ type OnlineStore struct { ServiceConfigs `json:",inline"` Persistence *OnlineStorePersistence `json:"persistence,omitempty"` + TLS *TlsConfigs `json:"tls,omitempty"` } // OnlineStorePersistence configures the persistence settings for the online store service @@ -163,9 +172,11 @@ var ValidOnlineStoreDBStorePersistenceTypes = []string{ type LocalRegistryConfig struct { ServiceConfigs `json:",inline"` Persistence *RegistryPersistence `json:"persistence,omitempty"` + TLS *TlsConfigs `json:"tls,omitempty"` } // RegistryPersistence configures the persistence settings for the registry service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." type RegistryPersistence struct { FilePersistence *RegistryFilePersistence `json:"file,omitempty"` DBPersistence *RegistryDBStorePersistence `json:"store,omitempty"` @@ -238,7 +249,8 @@ type RemoteRegistryConfig struct { // Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` Hostname *string `json:"hostname,omitempty"` // Reference to an existing `FeatureStore` CR in the same k8s cluster. - FeastRef *FeatureStoreRef `json:"feastRef,omitempty"` + FeastRef *FeatureStoreRef `json:"feastRef,omitempty"` + TLS *TlsRemoteRegistryConfigs `json:"tls,omitempty"` } // FeatureStoreRef defines which existing FeatureStore's registry should be used @@ -284,6 +296,45 @@ type KubernetesAuthz struct { Roles []string `json:"roles,omitempty"` } +// TlsConfigs configures server TLS for a feast service. in an openshift cluster, this is configured by default using service serving certificates. +// +kubebuilder:validation:XValidation:rule="(!has(self.disable) || !self.disable) ? has(self.secretRef) : true",message="`secretRef` required if `disable` is false." +type TlsConfigs struct { + // references the local k8s secret where the TLS key and cert reside + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + SecretKeyNames SecretKeyNames `json:"secretKeyNames,omitempty"` + // will disable TLS for the feast service. useful in an openshift cluster, for example, where TLS is configured by default + Disable *bool `json:"disable,omitempty"` +} + +// `secretRef` required if `disable` is false. +func (tls *TlsConfigs) IsTLS() bool { + if tls != nil { + if tls.Disable != nil && *tls.Disable { + return false + } else if tls.SecretRef == nil { + return false + } + return true + } + return false +} + +// TlsRemoteRegistryConfigs configures client TLS for a remote feast registry. in an openshift cluster, this is configured by default when the remote feast registry is using service serving certificates. +type TlsRemoteRegistryConfigs struct { + // references the local k8s configmap where the TLS cert resides + ConfigMapRef corev1.LocalObjectReference `json:"configMapRef"` + // defines the configmap key name for the client TLS cert. + CertName string `json:"certName"` +} + +// SecretKeyNames defines the secret key names for the TLS key and cert. +type SecretKeyNames struct { + // defaults to "tls.crt" + TlsCrt string `json:"tlsCrt,omitempty"` + // defaults to "tls.key" + TlsKey string `json:"tlsKey,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 0985e611cd8..8675397fee5 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -248,6 +248,11 @@ func (in *LocalRegistryConfig) DeepCopyInto(out *LocalRegistryConfig) { *out = new(RegistryPersistence) (*in).DeepCopyInto(*out) } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsConfigs) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRegistryConfig. @@ -269,6 +274,11 @@ func (in *OfflineStore) DeepCopyInto(out *OfflineStore) { *out = new(OfflineStorePersistence) (*in).DeepCopyInto(*out) } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(OfflineTlsConfigs) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStore. @@ -342,6 +352,27 @@ func (in *OfflineStorePersistence) DeepCopy() *OfflineStorePersistence { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineTlsConfigs) DeepCopyInto(out *OfflineTlsConfigs) { + *out = *in + in.TlsConfigs.DeepCopyInto(&out.TlsConfigs) + if in.VerifyClient != nil { + in, out := &in.VerifyClient, &out.VerifyClient + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineTlsConfigs. +func (in *OfflineTlsConfigs) DeepCopy() *OfflineTlsConfigs { + if in == nil { + return nil + } + out := new(OfflineTlsConfigs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = *in @@ -351,6 +382,11 @@ func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = new(OnlineStorePersistence) (*in).DeepCopyInto(*out) } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsConfigs) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStore. @@ -616,6 +652,11 @@ func (in *RemoteRegistryConfig) DeepCopyInto(out *RemoteRegistryConfig) { *out = new(FeatureStoreRef) **out = **in } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsRemoteRegistryConfigs) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteRegistryConfig. @@ -628,6 +669,21 @@ func (in *RemoteRegistryConfig) DeepCopy() *RemoteRegistryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyNames) DeepCopyInto(out *SecretKeyNames) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyNames. +func (in *SecretKeyNames) DeepCopy() *SecretKeyNames { + if in == nil { + return nil + } + out := new(SecretKeyNames) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceConfigs) DeepCopyInto(out *ServiceConfigs) { *out = *in @@ -659,3 +715,45 @@ func (in *ServiceHostnames) DeepCopy() *ServiceHostnames { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TlsConfigs) DeepCopyInto(out *TlsConfigs) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } + out.SecretKeyNames = in.SecretKeyNames + if in.Disable != nil { + in, out := &in.Disable, &out.Disable + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsConfigs. +func (in *TlsConfigs) DeepCopy() *TlsConfigs { + if in == nil { + return nil + } + out := new(TlsConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TlsRemoteRegistryConfigs) DeepCopyInto(out *TlsRemoteRegistryConfigs) { + *out = *in + out.ConfigMapRef = in.ConfigMapRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsRemoteRegistryConfigs. +func (in *TlsRemoteRegistryConfigs) DeepCopy() *TlsRemoteRegistryConfigs { + if in == nil { + return nil + } + out := new(TlsRemoteRegistryConfigs) + in.DeepCopyInto(out) + return out +} diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go index e132a6a3c9c..23a0309041b 100644 --- a/infra/feast-operator/cmd/main.go +++ b/infra/feast-operator/cmd/main.go @@ -38,6 +38,7 @@ import ( feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" //+kubebuilder:scaffold:imports ) @@ -132,6 +133,8 @@ func main() { os.Exit(1) } + services.SetIsOpenShift(mgr.GetConfig()) + if err = (&controller.FeatureStoreReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), 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 b2dd5c0f926..958c7cdddb1 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -385,6 +385,47 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: OfflineTlsConfigs configures server TLS for the + offline feast service. in an openshift cluster, this is + configured by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + verifyClient: + description: verify the client TLS certificate. + type: boolean + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object onlineStore: description: OnlineStore configures the deployed online store @@ -703,6 +744,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured by + default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object registry: description: Registry configures the registry service. One selection @@ -970,6 +1049,10 @@ spec: - type type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1027,6 +1110,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured + by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object remote: description: |- @@ -1050,6 +1171,32 @@ spec: description: Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. in an openshift cluster, + this is configured by default when the remote feast + registry is using service serving certificates. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object type: object x-kubernetes-validations: - message: One selection required. @@ -1411,6 +1558,48 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: OfflineTlsConfigs configures server TLS for + the offline feast service. in an openshift cluster, + this is configured by default using service serving + certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + verifyClient: + description: verify the client TLS certificate. + type: boolean + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object onlineStore: description: OnlineStore configures the deployed online store @@ -1734,6 +1923,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured + by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object registry: description: Registry configures the registry service. One @@ -2007,6 +2234,11 @@ spec: - type type: object type: object + x-kubernetes-validations: + - message: One selection required between file or + store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -2064,6 +2296,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. in an openshift cluster, this is + configured by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object remote: description: |- @@ -2087,6 +2357,32 @@ spec: description: Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. in an openshift + cluster, this is configured by default when the + remote feast registry is using service serving certificates. + properties: + certName: + description: defines the configmap key name for + the client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap + where the TLS cert resides + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object type: object x-kubernetes-validations: - message: One selection required. diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index 5e9ed7f7393..6bec442790b 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -65,6 +65,7 @@ rules: - apiGroups: - rbac.authorization.k8s.io resources: + - rolebindings - roles verbs: - create diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 731a48da65a..b5e103f9692 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -393,6 +393,47 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: OfflineTlsConfigs configures server TLS for the + offline feast service. in an openshift cluster, this is + configured by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + verifyClient: + description: verify the client TLS certificate. + type: boolean + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object onlineStore: description: OnlineStore configures the deployed online store @@ -711,6 +752,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured by + default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object registry: description: Registry configures the registry service. One selection @@ -978,6 +1057,10 @@ spec: - type type: object type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -1035,6 +1118,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured + by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object remote: description: |- @@ -1058,6 +1179,32 @@ spec: description: Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. in an openshift cluster, + this is configured by default when the remote feast + registry is using service serving certificates. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object type: object x-kubernetes-validations: - message: One selection required. @@ -1419,6 +1566,48 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: OfflineTlsConfigs configures server TLS for + the offline feast service. in an openshift cluster, + this is configured by default using service serving + certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + verifyClient: + description: verify the client TLS certificate. + type: boolean + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object onlineStore: description: OnlineStore configures the deployed online store @@ -1742,6 +1931,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. in an openshift cluster, this is configured + by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object registry: description: Registry configures the registry service. One @@ -2015,6 +2242,11 @@ spec: - type type: object type: object + x-kubernetes-validations: + - message: One selection required between file or + store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' resources: description: ResourceRequirements describes the compute resource requirements. @@ -2072,6 +2304,44 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. in an openshift cluster, this is + configured by default using service serving certificates. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' type: object remote: description: |- @@ -2095,6 +2365,32 @@ spec: description: Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. in an openshift + cluster, this is configured by default when the + remote feast registry is using service serving certificates. + properties: + certName: + description: defines the configmap key name for + the client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap + where the TLS cert resides + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object type: object x-kubernetes-validations: - message: One selection required. @@ -2371,6 +2667,7 @@ rules: - apiGroups: - rbac.authorization.k8s.io resources: + - rolebindings - roles verbs: - create diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index 59747d75bd2..c7d4b896063 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -17,35 +17,40 @@ import ( // 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()) + if authz.isKubernetesAuth() { + if err := authz.deployKubernetesAuth(); err != nil { + return err } + } else { + authz.removeOrphanedRoles() + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) + apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type) } return nil } -func (authz *FeastAuthorization) deployKubernetesAuth(kubernetesAuth *feastdevv1alpha1.KubernetesAuthz) error { - authz.removeOrphanedRoles() +func (authz *FeastAuthorization) isKubernetesAuth() bool { + authzConfig := authz.Handler.FeatureStore.Status.Applied.AuthzConfig + return authzConfig != nil && authzConfig.KubernetesAuthz != nil +} - if err := authz.createFeastRole(); err != nil { - return authz.setFeastKubernetesAuthCondition(err) - } - if err := authz.createFeastRoleBinding(); err != nil { - return authz.setFeastKubernetesAuthCondition(err) - } +func (authz *FeastAuthorization) deployKubernetesAuth() error { + if authz.isKubernetesAuth() { + authz.removeOrphanedRoles() - for _, roleName := range kubernetesAuth.Roles { - if err := authz.createAuthRole(roleName); err != nil { + if err := authz.createFeastRole(); err != nil { + return authz.setFeastKubernetesAuthCondition(err) + } + if err := authz.createFeastRoleBinding(); err != nil { return authz.setFeastKubernetesAuthCondition(err) } + + for _, roleName := range authz.Handler.FeatureStore.Status.Applied.AuthzConfig.KubernetesAuthz.Roles { + if err := authz.createAuthRole(roleName); err != nil { + return authz.setFeastKubernetesAuthCondition(err) + } + } } return authz.setFeastKubernetesAuthCondition(nil) } @@ -61,7 +66,7 @@ func (authz *FeastAuthorization) removeOrphanedRoles() { } desiredRoles := []string{} - if authz.Handler.FeatureStore.Status.Applied.AuthzConfig.KubernetesAuthz != nil { + if authz.isKubernetesAuth() { desiredRoles = authz.Handler.FeatureStore.Status.Applied.AuthzConfig.KubernetesAuthz.Roles } for _, role := range roleList.Items { diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index b90305b56b7..984bb7c9c26 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -57,7 +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=rbac.authorization.k8s.io,resources=roles;rolebindings,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 @@ -81,8 +81,6 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request } currentStatus := cr.Status.DeepCopy() - // initial status defaults must occur before feast deployment - services.ApplyDefaultsToStatus(cr) result, recErr = r.deployFeast(ctx, cr) if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) { if err = r.Client.Status().Update(ctx, cr); err != nil { @@ -110,8 +108,7 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Reason: feastdevv1alpha1.ReadyReason, Message: feastdevv1alpha1.ReadyMessage, } - - authz := authz.FeastAuthorization{ + feast := services.FeastServices{ Handler: feasthandler.FeastHandler{ Client: r.Client, Context: ctx, @@ -119,31 +116,26 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 Scheme: r.Scheme, }, } - if err = authz.Deploy(); err != nil { + authz := authz.FeastAuthorization{ + Handler: feast.Handler, + } + + // status defaults must be applied before deployments + errResult := ctrl.Result{Requeue: true, RequeueAfter: RequeueDelayError} + if err = feast.ApplyDefaults(); err != nil { + result = errResult + } else if err = authz.Deploy(); err != nil { + result = errResult + } else if err = feast.Deploy(); err != nil { + result = errResult + } + if 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} - } 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) 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 71713eb872f..796de8e5260 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -46,6 +46,7 @@ import ( var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Context("When deploying a resource with all ephemeral services", func() { const resourceName = "services-ephemeral" + const offlineType = "duckdb" var pullPolicy = corev1.PullAlways var testEnvVarName = "testEnvVarName" var testEnvVarValue = "testEnvVarValue" @@ -59,7 +60,6 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { featurestore := &feastdevv1alpha1.FeatureStore{} onlineStorePath := "/data/online.db" registryPath := "/data/registry.db" - offlineType := "duckdb" BeforeEach(func() { By("creating the custom resource for the Kind FeatureStore") @@ -127,7 +127,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { 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.AuthzConfig).To(BeNil()) 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,7 +217,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { 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 dce28f99118..db07418c92b 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -122,7 +122,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { 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.AuthzConfig).To(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) 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 33fde346385..d0adc62c7c8 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -153,7 +153,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { 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.AuthzConfig).To(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 980f3e36f5f..a6e71934fb0 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -45,6 +45,7 @@ import ( const feastProject = "test_project" const domain = ".svc.cluster.local:80" +const domainTls = ".svc.cluster.local:443" var image = "test:latest" @@ -133,7 +134,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.AuthzConfig).To(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore).To(BeNil()) @@ -151,6 +152,8 @@ var _ = Describe("FeatureStore Controller", func() { 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).To(BeNil()) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) Expect(cond).ToNot(BeNil()) @@ -188,7 +191,7 @@ var _ = Describe("FeatureStore Controller", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { @@ -458,7 +461,7 @@ var _ = Describe("FeatureStore Controller", func() { 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.AuthzConfig).To(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -549,7 +552,7 @@ var _ = Describe("FeatureStore Controller", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { @@ -961,6 +964,7 @@ var _ = Describe("FeatureStore Controller", func() { }, Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: referencedRegistry.Spec.FeastProject, + AuthzConfig: &feastdevv1alpha1.AuthzConfig{}, Services: &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{}, OfflineStore: &feastdevv1alpha1.OfflineStore{}, diff --git a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go new file mode 100644 index 00000000000..45cda317409 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go @@ -0,0 +1,489 @@ +/* +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" + "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "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" +) + +var _ = Describe("FeatureStore Controller - Feast service TLS", func() { + Context("When reconciling a FeatureStore resource", func() { + const resourceName = "test-tls" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + featurestore := &feastdevv1alpha1.FeatureStore{} + localRef := corev1.LocalObjectReference{Name: "test"} + tlsConfigs := feastdevv1alpha1.TlsConfigs{ + SecretRef: &localRef, + } + BeforeEach(func() { + By("creating the custom resource for the Kind FeatureStore") + err := k8sClient.Get(ctx, typeNamespacedName, featurestore) + if err != nil && errors.IsNotFound(err) { + resource := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + TLS: &tlsConfigs, + }, + OfflineStore: &feastdevv1alpha1.OfflineStore{ + TLS: &feastdevv1alpha1.OfflineTlsConfigs{ + TlsConfigs: tlsConfigs, + }, + }, + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + TLS: &tlsConfigs, + }, + }, + }, + }, + } + 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)) + Expect(resource.Status.Applied.Services).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry).NotTo(BeNil()) + + Expect(resource.Status.ServiceHostnames.OfflineStore).To(Equal(feast.GetFeastServiceName(services.OfflineFeastType) + "." + resource.Namespace + domainTls)) + Expect(resource.Status.ServiceHostnames.OnlineStore).To(Equal(feast.GetFeastServiceName(services.OnlineFeastType) + "." + resource.Namespace + domainTls)) + Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domainTls)) + + Expect(resource.Status.Conditions).NotTo(BeEmpty()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + svc := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpsPort)))) + }) + + 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()) + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + 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)) + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + RegistryType: services.RegistryFileConfigType, + Path: services.DefaultRegistryEphemeralPath, + }, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:443", resourceName), + Cert: services.GetTlsPath(services.RegistryFeastType) + "tls.crt", + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineFilePersistenceDaskConfigType, + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpsPort, + Scheme: services.HttpsScheme, + Cert: services.GetTlsPath(services.OfflineFeastType) + "tls.crt", + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Path: services.DefaultOnlineStoreEphemeralPath, + Type: services.OnlineSqliteConfigType, + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + + // 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("https://feast-%s-online.default.svc.cluster.local:443", resourceName), + Type: services.OnlineRemoteConfigType, + Cert: services.GetTlsPath(services.OnlineFeastType) + "tls.crt", + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change tls and reconcile + resourceNew := resource.DeepCopy() + disable := true + remoteRegHost := "test.other-ns:443" + resourceNew.Spec = feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + TLS: &feastdevv1alpha1.TlsConfigs{ + Disable: &disable, + }, + }, + OfflineStore: &feastdevv1alpha1.OfflineStore{ + TLS: &feastdevv1alpha1.OfflineTlsConfigs{ + TlsConfigs: tlsConfigs, + }, + }, + Registry: &feastdevv1alpha1.Registry{ + Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + Hostname: &remoteRegHost, + TLS: &feastdevv1alpha1.TlsRemoteRegistryConfigs{ + ConfigMapRef: localRef, + CertName: "remote.crt", + }, + }, + }, + }, + } + 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 registry + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote = services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: remoteRegHost, + Cert: services.GetTlsPath(services.RegistryFeastType) + "remote.crt", + } + offlineConfig.Registry = regRemote + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + + repoConfigOnline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + onlineConfig.Registry = regRemote + Expect(repoConfigOnline).To(Equal(onlineConfig)) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index e8faa59f21e..48ca0751cee 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -55,3 +55,34 @@ func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { feast.Handler.FeatureStore.Status.ClientConfigMap = cm.Name return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cm, feast.Handler.Scheme) } + +func (feast *FeastServices) createCaConfigMap() error { + logger := log.FromContext(feast.Handler.Context) + cm := feast.initCaConfigMap() + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, cm, controllerutil.MutateFn(func() error { + return feast.setCaConfigMap(cm) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ConfigMap", cm.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) setCaConfigMap(cm *corev1.ConfigMap) error { + cm.Labels = map[string]string{ + NameLabelKey: feast.Handler.FeatureStore.Name, + } + cm.Annotations = map[string]string{ + "service.beta.openshift.io/inject-cabundle": "true", + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cm, feast.Handler.Scheme) +} + +func (feast *FeastServices) initCaConfigMap() *corev1.ConfigMap { + cm := &corev1.ConfigMap{ + ObjectMeta: feast.GetObjectMeta(ClientCaFeastType), + } + cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) + return cm +} diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 6e8bd5f0482..8b52296160f 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -24,7 +24,6 @@ import ( feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" "gopkg.in/yaml.v3" - corev1 "k8s.io/api/core/v1" ) // GetServiceFeatureStoreYamlBase64 returns a base64 encoded feature_store.yaml config for the feast service @@ -52,7 +51,6 @@ func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1al appliedSpec := featureStore.Status.Applied repoConfig := getClientRepoConfig(featureStore) - isLocalRegistry := IsLocalRegistry(featureStore) if appliedSpec.Services != nil { services := appliedSpec.Services @@ -75,7 +73,7 @@ func getServiceRepoConfig(feastType FeastServiceType, featureStore *feastdevv1al } case RegistryFeastType: // Registry server only has a `registry` section - if isLocalRegistry { + if IsLocalRegistry(featureStore) { err := setRepoConfigRegistry(services, secretExtractionFunc, &repoConfig) if err != nil { return repoConfig, err @@ -208,6 +206,7 @@ func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig { status := featureStore.Status + appliedServices := status.Applied.Services clientRepoConfig := RepoConfig{ Project: status.Applied.FeastProject, Provider: LocalProviderType, @@ -219,11 +218,22 @@ func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig Host: strings.Split(status.ServiceHostnames.OfflineStore, ":")[0], Port: HttpPort, } + if appliedServices.OfflineStore != nil && appliedServices.OfflineStore.TLS != nil && + (&appliedServices.OfflineStore.TLS.TlsConfigs).IsTLS() { + clientRepoConfig.OfflineStore.Cert = GetTlsPath(OfflineFeastType) + appliedServices.OfflineStore.TLS.TlsConfigs.SecretKeyNames.TlsCrt + clientRepoConfig.OfflineStore.Port = HttpsPort + clientRepoConfig.OfflineStore.Scheme = HttpsScheme + } } if len(status.ServiceHostnames.OnlineStore) > 0 { + onlinePath := "://" + status.ServiceHostnames.OnlineStore clientRepoConfig.OnlineStore = OnlineStoreConfig{ Type: OnlineRemoteConfigType, - Path: strings.ToLower(string(corev1.URISchemeHTTP)) + "://" + status.ServiceHostnames.OnlineStore, + Path: HttpScheme + onlinePath, + } + if appliedServices.OnlineStore != nil && appliedServices.OnlineStore.TLS.IsTLS() { + clientRepoConfig.OnlineStore.Cert = GetTlsPath(OnlineFeastType) + appliedServices.OnlineStore.TLS.SecretKeyNames.TlsCrt + clientRepoConfig.OnlineStore.Path = HttpsScheme + onlinePath } } if len(status.ServiceHostnames.Registry) > 0 { @@ -231,18 +241,18 @@ func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig RegistryType: RegistryRemoteConfigType, Path: status.ServiceHostnames.Registry, } + if localRegistryTls(featureStore) { + clientRepoConfig.Registry.Cert = GetTlsPath(RegistryFeastType) + appliedServices.Registry.Local.TLS.SecretKeyNames.TlsCrt + } else if remoteRegistryTls(featureStore) { + clientRepoConfig.Registry.Cert = GetTlsPath(RegistryFeastType) + appliedServices.Registry.Remote.TLS.CertName + } } - if status.Applied.AuthzConfig.KubernetesAuthz == nil { - clientRepoConfig.AuthzConfig = AuthzConfig{ - Type: NoAuthAuthType, - } - } else { - if status.Applied.AuthzConfig.KubernetesAuthz != nil { - clientRepoConfig.AuthzConfig = AuthzConfig{ - Type: KubernetesAuthType, - } - } + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: NoAuthAuthType, + } + if status.Applied.AuthzConfig != nil && status.Applied.AuthzConfig.KubernetesAuthz != nil { + clientRepoConfig.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 1a87b118ee5..18cf104eb52 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -351,6 +351,16 @@ func minimalFeatureStore() *feastdevv1alpha1.FeatureStore { } } +func minimalFeatureStoreWithAllServices() *feastdevv1alpha1.FeatureStore { + feast := minimalFeatureStore() + feast.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{}, + OnlineStore: &feastdevv1alpha1.OnlineStore{}, + Registry: &feastdevv1alpha1.Registry{}, + } + return feast +} + func emptyMockExtractConfigFromSecret(secretRef string, secretKeyName string) (map[string]interface{}, error) { return map[string]interface{}{}, nil } diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 55cb7079816..c360581cb6e 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -35,63 +35,75 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +// Apply defaults and set service hostnames in FeatureStore status +func (feast *FeastServices) ApplyDefaults() error { + ApplyDefaultsToStatus(feast.Handler.FeatureStore) + if err := feast.setTlsDefaults(); err != nil { + return err + } + if err := feast.setServiceHostnames(); err != nil { + return err + } + return nil +} + // Deploy the feast services func (feast *FeastServices) Deploy() error { - if err := feast.setServiceHostnames(); err != nil { + openshiftTls, err := feast.checkOpenshiftTls() + if err != nil { return err } + if openshiftTls { + if err := feast.createCaConfigMap(); err != nil { + return err + } + } else { + _ = feast.Handler.DeleteOwnedFeastObj(feast.initCaConfigMap()) + } services := feast.Handler.FeatureStore.Status.Applied.Services - if services != nil { - if services.OfflineStore != nil { - offlinePersistence := services.OfflineStore.Persistence - - err := feast.validateOfflineStorePersistence(offlinePersistence) - if err != nil { - return err - } - - if err = feast.deployFeastServiceByType(OfflineFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OfflineFeastType); err != nil { - return err - } + if feast.isOfflinStore() { + err := feast.validateOfflineStorePersistence(services.OfflineStore.Persistence) + if err != nil { + return err } - if services.OnlineStore != nil { - onlinePersistence := services.OnlineStore.Persistence - - err := feast.validateOnlineStorePersistence(onlinePersistence) - if err != nil { - return err - } + if err = feast.deployFeastServiceByType(OfflineFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(OfflineFeastType); err != nil { + return err + } + } - if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(OnlineFeastType); err != nil { - return err - } + if feast.isOnlinStore() { + err := feast.validateOnlineStorePersistence(services.OnlineStore.Persistence) + if err != nil { + return err } - if feast.isLocalRegistry() { - registryPersistence := services.Registry.Local.Persistence + if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(OnlineFeastType); err != nil { + return err + } + } - err := feast.validateRegistryPersistence(registryPersistence) - if err != nil { - return err - } + if feast.isLocalRegistry() { + err := feast.validateRegistryPersistence(services.Registry.Local.Persistence) + if err != nil { + return err + } - if err = feast.deployFeastServiceByType(RegistryFeastType); err != nil { - return err - } - } else { - if err := feast.removeFeastServiceByType(RegistryFeastType); err != nil { - return err - } + if err = feast.deployFeastServiceByType(RegistryFeastType); err != nil { + return err + } + } else { + if err := feast.removeFeastServiceByType(RegistryFeastType); err != nil { + return err } } @@ -179,13 +191,13 @@ func (feast *FeastServices) deployFeastServiceByType(feastType FeastServiceType) } else { _ = feast.Handler.DeleteOwnedFeastObj(feast.initPVC(feastType)) } - if err := feast.createServiceAccount(feastType); err != nil { + if err := feast.createService(feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) } - if err := feast.createDeployment(feastType); err != nil { + if err := feast.createServiceAccount(feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) } - if err := feast.createService(feastType); err != nil { + if err := feast.createDeployment(feastType); err != nil { return feast.setFeastServiceCondition(err, feastType) } return feast.setFeastServiceCondition(nil, feastType) @@ -255,6 +267,7 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, fea return err } + // PVCs are immutable, so we only create... we don't update an existing one. err = feast.Handler.Client.Get(feast.Handler.Context, client.ObjectKeyFromObject(pvc), pvc) if err != nil && apierrors.IsNotFound(err) { err = feast.Handler.Client.Create(feast.Handler.Context, pvc) @@ -273,17 +286,12 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType F return err } deploy.Labels = feast.getLabels(feastType) - deploySettings := FeastServiceConstants[feastType] + sa := feast.initFeastSA(feastType) + tls := feast.getTlsConfigs(feastType) serviceConfigs := feast.getServiceConfigs(feastType) defaultServiceConfigs := serviceConfigs.DefaultConfigs - sa := feast.initFeastSA(feastType) + probeHandler := getProbeHandler(feastType, tls) - // standard configs are applied here - probeHandler := corev1.ProbeHandler{ - TCPSocket: &corev1.TCPSocketAction{ - Port: intstr.FromInt(int(deploySettings.TargetPort)), - }, - } deploy.Spec = appsv1.DeploymentSpec{ Replicas: &DefaultReplicas, Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), @@ -297,11 +305,11 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType F { Name: string(feastType), Image: *defaultServiceConfigs.Image, - Command: deploySettings.Command, + Command: feast.getContainerCommand(feastType), Ports: []corev1.ContainerPort{ { Name: string(feastType), - ContainerPort: deploySettings.TargetPort, + ContainerPort: getTargetPort(feastType, tls), Protocol: corev1.ProtocolTCP, }, }, @@ -319,7 +327,7 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType F ReadinessProbe: &corev1.Probe{ ProbeHandler: probeHandler, InitialDelaySeconds: 20, - PeriodSeconds: 10, + PeriodSeconds: 30, }, }, }, @@ -328,28 +336,100 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment, feastType F } // configs are applied here - container := &deploy.Spec.Template.Spec.Containers[0] - applyOptionalContainerConfigs(container, serviceConfigs.OptionalConfigs) + podSpec := &deploy.Spec.Template.Spec + applyOptionalContainerConfigs(&podSpec.Containers[0], serviceConfigs.OptionalConfigs) + feast.mountTlsConfig(feastType, podSpec) if pvcConfig, hasPvcConfig := hasPvcConfig(feast.Handler.FeatureStore, feastType); hasPvcConfig { - mountPvcConfig(&deploy.Spec.Template.Spec, pvcConfig, deploy.Name) + mountPvcConfig(podSpec, pvcConfig, deploy.Name) + } + + switch feastType { + case OfflineFeastType: + feast.registryClientPodConfigs(podSpec) + case OnlineFeastType: + feast.registryClientPodConfigs(podSpec) + feast.offlineClientPodConfigs(podSpec) } return controllerutil.SetControllerReference(feast.Handler.FeatureStore, deploy, feast.Handler.Scheme) } +func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []string { + deploySettings := FeastServiceConstants[feastType] + targetPort := deploySettings.TargetHttpPort + tls := feast.getTlsConfigs(feastType) + if tls.IsTLS() { + targetPort = deploySettings.TargetHttpsPort + feastTlsPath := GetTlsPath(feastType) + deploySettings.Command = append(deploySettings.Command, []string{"--key", feastTlsPath + tls.SecretKeyNames.TlsKey, + "--cert", feastTlsPath + tls.SecretKeyNames.TlsCrt}...) + } + deploySettings.Command = append(deploySettings.Command, []string{"-p", strconv.Itoa(int(targetPort))}...) + + if feastType == OfflineFeastType { + if tls.IsTLS() && feast.Handler.FeatureStore.Status.Applied.Services.OfflineStore.TLS.VerifyClient != nil { + deploySettings.Command = append(deploySettings.Command, + []string{"--verify_client", strconv.FormatBool(*feast.Handler.FeatureStore.Status.Applied.Services.OfflineStore.TLS.VerifyClient)}...) + } + } + + return deploySettings.Command +} + +func (feast *FeastServices) offlineClientPodConfigs(podSpec *corev1.PodSpec) { + feast.mountTlsConfig(OfflineFeastType, podSpec) +} + +func (feast *FeastServices) registryClientPodConfigs(podSpec *corev1.PodSpec) { + feast.setRegistryClientInitContainer(podSpec) + feast.mountRegistryClientTls(podSpec) +} + +func (feast *FeastServices) setRegistryClientInitContainer(podSpec *corev1.PodSpec) { + hostname := feast.Handler.FeatureStore.Status.ServiceHostnames.Registry + if len(hostname) > 0 { + grpcurlFlag := "-plaintext" + hostSplit := strings.Split(hostname, ":") + if len(hostSplit) > 1 && hostSplit[1] == "443" { + grpcurlFlag = "-insecure" + } + podSpec.InitContainers = []corev1.Container{ + { + Name: "init-registry", + Image: "fullstorydev/grpcurl:v1.9.1-alpine", + Command: []string{ + "sh", "-c", + "until grpcurl " + grpcurlFlag + " -d '' -format text " + hostname + " grpc.health.v1.Health/Check; do echo waiting for registry; sleep 2; done", + }, + }, + } + } +} + func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType) error { svc.Labels = feast.getLabels(feastType) - deploySettings := FeastServiceConstants[feastType] + if feast.isOpenShiftTls(feastType) { + svc.Annotations = map[string]string{ + "service.beta.openshift.io/serving-cert-secret-name": svc.Name + tlsNameSuffix, + } + } + var port int32 = HttpPort + scheme := HttpScheme + tls := feast.getTlsConfigs(feastType) + if tls.IsTLS() { + port = HttpsPort + scheme = HttpsScheme + } svc.Spec = corev1.ServiceSpec{ Selector: svc.GetLabels(), Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { - Name: strings.ToLower(string(corev1.URISchemeHTTP)), - Port: HttpPort, + Name: scheme, + Port: port, Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromInt(int(deploySettings.TargetPort)), + TargetPort: intstr.FromInt(int(getTargetPort(feastType, tls))), }, }, } @@ -376,21 +456,19 @@ func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1alpha1.PvcCreate, } func (feast *FeastServices) getServiceConfigs(feastType FeastServiceType) feastdevv1alpha1.ServiceConfigs { - appliedSpec := feast.Handler.FeatureStore.Status.Applied + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services switch feastType { case OfflineFeastType: - if appliedSpec.Services.OfflineStore != nil { - return appliedSpec.Services.OfflineStore.ServiceConfigs + if feast.isOfflinStore() { + return appliedServices.OfflineStore.ServiceConfigs } case OnlineFeastType: - if appliedSpec.Services.OnlineStore != nil { - return appliedSpec.Services.OnlineStore.ServiceConfigs + if feast.isOnlinStore() { + return appliedServices.OnlineStore.ServiceConfigs } case RegistryFeastType: - if appliedSpec.Services.Registry != nil { - if appliedSpec.Services.Registry.Local != nil { - return appliedSpec.Services.Registry.Local.ServiceConfigs - } + if feast.isLocalRegistry() { + return appliedServices.Registry.Local.ServiceConfigs } } return feastdevv1alpha1.ServiceConfigs{} @@ -423,23 +501,26 @@ func (feast *FeastServices) getLabels(feastType FeastServiceType) map[string]str func (feast *FeastServices) setServiceHostnames() error { 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.Handler.FeatureStore.Status.ServiceHostnames.OfflineStore = objMeta.Name + "." + objMeta.Namespace + domain - } - if services.OnlineStore != nil { - objMeta := feast.GetObjectMeta(OnlineFeastType) - feast.Handler.FeatureStore.Status.ServiceHostnames.OnlineStore = objMeta.Name + "." + objMeta.Namespace + domain - } - if feast.isLocalRegistry() { - objMeta := feast.GetObjectMeta(RegistryFeastType) - feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = objMeta.Name + "." + objMeta.Namespace + domain - } else if feast.isRemoteRegistry() { - return feast.setRemoteRegistryURL() + domain := svcDomain + ":" + if feast.isOfflinStore() { + objMeta := feast.GetObjectMeta(OfflineFeastType) + port := strconv.Itoa(HttpPort) + if feast.offlineTls() { + port = strconv.Itoa(HttpsPort) } + feast.Handler.FeatureStore.Status.ServiceHostnames.OfflineStore = objMeta.Name + "." + objMeta.Namespace + domain + port + } + if feast.isOnlinStore() { + objMeta := feast.GetObjectMeta(OnlineFeastType) + feast.Handler.FeatureStore.Status.ServiceHostnames.OnlineStore = objMeta.Name + "." + objMeta.Namespace + domain + + getPortStr(feast.Handler.FeatureStore.Status.Applied.Services.OnlineStore.TLS) + } + if feast.isLocalRegistry() { + objMeta := feast.GetObjectMeta(RegistryFeastType) + feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = objMeta.Name + "." + objMeta.Namespace + domain + + getPortStr(feast.Handler.FeatureStore.Status.Applied.Services.Registry.Local.TLS) + } else if feast.isRemoteRegistry() { + return feast.setRemoteRegistryURL() } return nil } @@ -463,42 +544,51 @@ func (feast *FeastServices) setRemoteRegistryURL() error { if feast.isRemoteHostnameRegistry() { feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = *feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote.Hostname } else if feast.IsRemoteRefRegistry() { + remoteFeast, err := feast.getRemoteRegistryFeastHandler() + if err != nil { + return err + } + // referenced/remote registry must use the local install option and be in a 'Ready' state. + if remoteFeast != nil && + remoteFeast.isLocalRegistry() && + apimeta.IsStatusConditionTrue(remoteFeast.Handler.FeatureStore.Status.Conditions, feastdevv1alpha1.RegistryReadyType) { + feast.Handler.FeatureStore.Status.ServiceHostnames.Registry = remoteFeast.Handler.FeatureStore.Status.ServiceHostnames.Registry + } else { + return errors.New("Remote feast registry of referenced FeatureStore '" + remoteFeast.Handler.FeatureStore.Name + "' is not ready") + } + } + return nil +} + +func (feast *FeastServices) getRemoteRegistryFeastHandler() (*FeastServices, error) { + if feast.IsRemoteRefRegistry() { 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.Handler.FeatureStore.Namespace } - nsName := types.NamespacedName{Name: feastRemoteRef.Name, Namespace: feastRemoteRef.Namespace} 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`") + return nil, errors.New("FeatureStore '" + crNsName.Name + "' can't reference itself in `spec.services.registry.remote.feastRef`") } - remoteFeastObj := &feastdevv1alpha1.FeatureStore{} 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") + return nil, errors.New("Referenced FeatureStore '" + feastRemoteRef.Name + "' was not found") } - return err + return nil, err } - - remoteFeast := FeastServices{ + return &FeastServices{ 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.Handler.FeatureStore.Status.ServiceHostnames.Registry = remoteFeastObj.Status.ServiceHostnames.Registry - } else { - return errors.New("Remote feast registry of referenced FeatureStore '" + feastRemoteRef.Name + "' is not ready") - } + }, nil } - return nil + return nil, nil } func (feast *FeastServices) isLocalRegistry() bool { @@ -506,8 +596,7 @@ func (feast *FeastServices) isLocalRegistry() bool { } func (feast *FeastServices) isRemoteRegistry() bool { - appliedServices := feast.Handler.FeatureStore.Status.Applied.Services - return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Remote != nil + return isRemoteRegistry(feast.Handler.FeatureStore) } func (feast *FeastServices) IsRemoteRefRegistry() bool { @@ -526,6 +615,16 @@ func (feast *FeastServices) isRemoteHostnameRegistry() bool { return false } +func (feast *FeastServices) isOfflinStore() bool { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.OfflineStore != nil +} + +func (feast *FeastServices) isOnlinStore() bool { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.OnlineStore != nil +} + func (feast *FeastServices) initFeastDeploy(feastType FeastServiceType) *appsv1.Deployment { deploy := &appsv1.Deployment{ ObjectMeta: feast.GetObjectMeta(feastType), @@ -593,28 +692,54 @@ func mergeEnvVarsArrays(envVars1 []corev1.EnvVar, envVars2 *[]corev1.EnvVar) []c } func mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *feastdevv1alpha1.PvcConfig, deployName string) { - container := &podSpec.Containers[0] - var pvcName string - if pvcConfig.Create != nil { - pvcName = deployName - } else { - pvcName = pvcConfig.Ref.Name - } + if podSpec != nil && pvcConfig != nil { + container := &podSpec.Containers[0] + var pvcName string + if pvcConfig.Create != nil { + pvcName = deployName + } else { + pvcName = pvcConfig.Ref.Name + } - podSpec.Volumes = []corev1.Volume{ - { + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ Name: pvcName, VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: pvcName, }, }, - }, - } - container.VolumeMounts = []corev1.VolumeMount{ - { + }) + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ Name: pvcName, MountPath: pvcConfig.MountPath, + }) + } +} + +func getTargetPort(feastType FeastServiceType, tls *feastdevv1alpha1.TlsConfigs) int32 { + if tls.IsTLS() { + return FeastServiceConstants[feastType].TargetHttpsPort + } + return FeastServiceConstants[feastType].TargetHttpPort +} + +func getProbeHandler(feastType FeastServiceType, tls *feastdevv1alpha1.TlsConfigs) corev1.ProbeHandler { + targetPort := getTargetPort(feastType, tls) + if feastType == OnlineFeastType { + probeHandler := corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/health", + Port: intstr.FromInt(int(targetPort)), + }, + } + if tls.IsTLS() { + probeHandler.HTTPGet.Scheme = corev1.URISchemeHTTPS + } + return probeHandler + } + return corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(int(targetPort)), }, } } diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 2e5b1f1a4e5..65e50b36818 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -31,7 +31,13 @@ const ( DefaultOnlineStoreEphemeralPath = "/tmp/online_store.db" DefaultOnlineStorePvcPath = "online_store.db" svcDomain = ".svc.cluster.local" - HttpPort = 80 + + HttpPort = 80 + HttpsPort = 443 + HttpScheme = "http" + HttpsScheme = "https" + tlsPath = "/tls/" + tlsNameSuffix = "-tls" DefaultOfflineStorageRequest = "20Gi" DefaultOnlineStorageRequest = "5Gi" @@ -41,6 +47,7 @@ const ( OnlineFeastType FeastServiceType = "online" RegistryFeastType FeastServiceType = "registry" ClientFeastType FeastServiceType = "client" + ClientCaFeastType FeastServiceType = "client-ca" OfflineRemoteConfigType OfflineConfigType = "remote" OfflineFilePersistenceDaskConfigType OfflineConfigType = "dask" @@ -71,16 +78,19 @@ var ( FeastServiceConstants = map[FeastServiceType]deploymentSettings{ OfflineFeastType: { - Command: []string{"feast", "serve_offline", "-h", "0.0.0.0"}, - TargetPort: 8815, + Command: []string{"feast", "serve_offline", "-h", "0.0.0.0"}, + TargetHttpPort: 8815, + TargetHttpsPort: 8816, }, OnlineFeastType: { - Command: []string{"feast", "serve", "-h", "0.0.0.0"}, - TargetPort: 6566, + Command: []string{"feast", "serve", "-h", "0.0.0.0"}, + TargetHttpPort: 6566, + TargetHttpsPort: 6567, }, RegistryFeastType: { - Command: []string{"feast", "serve_registry"}, - TargetPort: 6570, + Command: []string{"feast", "serve_registry"}, + TargetHttpPort: 6570, + TargetHttpsPort: 6571, }, } @@ -180,6 +190,8 @@ type OfflineStoreConfig struct { Host string `yaml:"host,omitempty"` Type OfflineConfigType `yaml:"type,omitempty"` Port int `yaml:"port,omitempty"` + Scheme string `yaml:"scheme,omitempty"` + Cert string `yaml:"cert,omitempty"` DBParameters map[string]interface{} `yaml:",inline,omitempty"` } @@ -187,6 +199,7 @@ type OfflineStoreConfig struct { type OnlineStoreConfig struct { Path string `yaml:"path,omitempty"` Type OnlineConfigType `yaml:"type,omitempty"` + Cert string `yaml:"cert,omitempty"` DBParameters map[string]interface{} `yaml:",inline,omitempty"` } @@ -194,6 +207,7 @@ type OnlineStoreConfig struct { type RegistryConfig struct { Path string `yaml:"path,omitempty"` RegistryType RegistryConfigType `yaml:"registry_type,omitempty"` + Cert string `yaml:"cert,omitempty"` S3AdditionalKwargs *map[string]string `yaml:"s3_additional_kwargs,omitempty"` DBParameters map[string]interface{} `yaml:",inline,omitempty"` } @@ -204,6 +218,7 @@ type AuthzConfig struct { } type deploymentSettings struct { - Command []string - TargetPort int32 + Command []string + TargetHttpPort int32 + TargetHttpsPort int32 } diff --git a/infra/feast-operator/internal/controller/services/suite_test.go b/infra/feast-operator/internal/controller/services/suite_test.go index 5f76f5e6ff7..e1e485f1bf6 100644 --- a/infra/feast-operator/internal/controller/services/suite_test.go +++ b/infra/feast-operator/internal/controller/services/suite_test.go @@ -84,3 +84,7 @@ var _ = AfterSuite(func() { err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) }) + +func testSetIsOpenShift() { + isOpenShift = true +} diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go new file mode 100644 index 00000000000..9cab14faa7d --- /dev/null +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -0,0 +1,245 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "strconv" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +func (feast *FeastServices) setTlsDefaults() error { + if err := feast.setOpenshiftTls(); err != nil { + return err + } + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + if feast.isOfflinStore() && appliedServices.OfflineStore.TLS != nil { + tlsDefaults(&appliedServices.OfflineStore.TLS.TlsConfigs) + } + if feast.isOnlinStore() { + tlsDefaults(appliedServices.OnlineStore.TLS) + } + if feast.isLocalRegistry() { + tlsDefaults(appliedServices.Registry.Local.TLS) + } + return nil +} + +func (feast *FeastServices) setOpenshiftTls() error { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + tlsConfigs := &feastdevv1alpha1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{}, + } + if feast.offlineOpenshiftTls() { + appliedServices.OfflineStore.TLS = &feastdevv1alpha1.OfflineTlsConfigs{ + TlsConfigs: *tlsConfigs, + } + appliedServices.OfflineStore.TLS.TlsConfigs.SecretRef.Name = feast.initFeastSvc(OfflineFeastType).Name + tlsNameSuffix + } + if feast.onlineOpenshiftTls() { + appliedServices.OnlineStore.TLS = tlsConfigs + appliedServices.OnlineStore.TLS.SecretRef.Name = feast.initFeastSvc(OnlineFeastType).Name + tlsNameSuffix + } + if feast.localRegistryOpenshiftTls() { + appliedServices.Registry.Local.TLS = tlsConfigs + appliedServices.Registry.Local.TLS.SecretRef.Name = feast.initFeastSvc(RegistryFeastType).Name + tlsNameSuffix + } else if remote, err := feast.remoteRegistryOpenshiftTls(); remote { + // if the remote registry reference is using openshift's service serving certificates, we can use the injected service CA bundle configMap + if appliedServices.Registry.Remote.TLS == nil { + appliedServices.Registry.Remote.TLS = &feastdevv1alpha1.TlsRemoteRegistryConfigs{ + ConfigMapRef: corev1.LocalObjectReference{ + Name: feast.initCaConfigMap().Name, + }, + CertName: "service-ca.crt", + } + } + } else if err != nil { + return err + } + return nil +} + +func (feast *FeastServices) checkOpenshiftTls() (bool, error) { + if feast.offlineOpenshiftTls() || feast.onlineOpenshiftTls() || feast.localRegistryOpenshiftTls() { + return true, nil + } + return feast.remoteRegistryOpenshiftTls() +} + +func (feast *FeastServices) isOpenShiftTls(feastType FeastServiceType) (isOpenShift bool) { + switch feastType { + case OfflineFeastType: + isOpenShift = feast.offlineOpenshiftTls() + case OnlineFeastType: + isOpenShift = feast.onlineOpenshiftTls() + case RegistryFeastType: + isOpenShift = feast.localRegistryOpenshiftTls() + } + return +} + +func (feast *FeastServices) getTlsConfigs(feastType FeastServiceType) (tls *feastdevv1alpha1.TlsConfigs) { + appliedServices := feast.Handler.FeatureStore.Status.Applied.Services + switch feastType { + case OfflineFeastType: + if feast.isOfflinStore() && appliedServices.OfflineStore.TLS != nil { + tls = &appliedServices.OfflineStore.TLS.TlsConfigs + } + case OnlineFeastType: + if feast.isOnlinStore() { + tls = appliedServices.OnlineStore.TLS + } + case RegistryFeastType: + if feast.isLocalRegistry() { + tls = appliedServices.Registry.Local.TLS + } + } + return +} + +// True if running in an openshift cluster and Tls not configured in the service Spec +func (feast *FeastServices) offlineOpenshiftTls() bool { + return isOpenShift && + feast.isOfflinStore() && feast.Handler.FeatureStore.Spec.Services.OfflineStore.TLS == nil +} + +// True if running in an openshift cluster and Tls not configured in the service Spec +func (feast *FeastServices) onlineOpenshiftTls() bool { + return isOpenShift && + feast.isOnlinStore() && feast.Handler.FeatureStore.Spec.Services.OnlineStore.TLS == nil +} + +// True if running in an openshift cluster and Tls not configured in the service Spec +func (feast *FeastServices) localRegistryOpenshiftTls() bool { + return isOpenShift && + feast.isLocalRegistry() && + (feast.Handler.FeatureStore.Spec.Services == nil || + feast.Handler.FeatureStore.Spec.Services.Registry == nil || + feast.Handler.FeatureStore.Spec.Services.Registry.Local == nil || + feast.Handler.FeatureStore.Spec.Services.Registry.Local.TLS == nil) +} + +// True if running in an openshift cluster, and using a remote registry in the same cluster, with no remote Tls set in the service Spec +func (feast *FeastServices) remoteRegistryOpenshiftTls() (bool, error) { + if isOpenShift && feast.isRemoteRegistry() { + remoteFeast, err := feast.getRemoteRegistryFeastHandler() + if err != nil { + return false, err + } + return (remoteFeast != nil && remoteFeast.localRegistryOpenshiftTls() && + feast.Handler.FeatureStore.Spec.Services.Registry.Remote.TLS == nil), + nil + } + return false, nil +} + +func (feast *FeastServices) offlineTls() bool { + return feast.isOfflinStore() && + feast.Handler.FeatureStore.Status.Applied.Services.OfflineStore.TLS != nil && + (&feast.Handler.FeatureStore.Status.Applied.Services.OfflineStore.TLS.TlsConfigs).IsTLS() +} + +func (feast *FeastServices) localRegistryTls() bool { + return localRegistryTls(feast.Handler.FeatureStore) +} + +func (feast *FeastServices) remoteRegistryTls() bool { + return remoteRegistryTls(feast.Handler.FeatureStore) +} + +func (feast *FeastServices) mountRegistryClientTls(podSpec *corev1.PodSpec) { + if podSpec != nil { + if feast.localRegistryTls() { + feast.mountTlsConfig(RegistryFeastType, podSpec) + } else if feast.remoteRegistryTls() { + mountTlsRemoteRegistryConfig(RegistryFeastType, podSpec, + feast.Handler.FeatureStore.Status.Applied.Services.Registry.Remote.TLS) + } + } +} + +func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec *corev1.PodSpec) { + tls := feast.getTlsConfigs(feastType) + if tls.IsTLS() && podSpec != nil { + volName := string(feastType) + tlsNameSuffix + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: tls.SecretRef.Name, + }, + }, + }) + container := &podSpec.Containers[0] + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(feastType), + ReadOnly: true, + }) + } +} + +func mountTlsRemoteRegistryConfig(feastType FeastServiceType, podSpec *corev1.PodSpec, tls *feastdevv1alpha1.TlsRemoteRegistryConfigs) { + if tls != nil { + volName := string(feastType) + tlsNameSuffix + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: tls.ConfigMapRef, + }, + }, + }) + container := &podSpec.Containers[0] + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(feastType), + ReadOnly: true, + }) + } +} + +func getPortStr(tls *feastdevv1alpha1.TlsConfigs) string { + if tls.IsTLS() { + return strconv.Itoa(HttpsPort) + } + return strconv.Itoa(HttpPort) +} + +func tlsDefaults(tls *feastdevv1alpha1.TlsConfigs) { + if tls.IsTLS() { + if len(tls.SecretKeyNames.TlsCrt) == 0 { + tls.SecretKeyNames.TlsCrt = "tls.crt" + } + if len(tls.SecretKeyNames.TlsKey) == 0 { + tls.SecretKeyNames.TlsKey = "tls.key" + } + } +} + +func localRegistryTls(featureStore *feastdevv1alpha1.FeatureStore) bool { + return IsLocalRegistry(featureStore) && featureStore.Status.Applied.Services.Registry.Local.TLS.IsTLS() +} + +func remoteRegistryTls(featureStore *feastdevv1alpha1.FeatureStore) bool { + return isRemoteRegistry(featureStore) && featureStore.Status.Applied.Services.Registry.Remote.TLS != nil +} + +func GetTlsPath(feastType FeastServiceType) string { + return tlsPath + string(feastType) + "/" +} diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go new file mode 100644 index 00000000000..28a3a49ec2f --- /dev/null +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -0,0 +1,276 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +// test tls functions directly +var _ = Describe("TLS Config", func() { + Context("When reconciling a FeatureStore", func() { + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(feastdevv1alpha1.AddToScheme(scheme)) + + secretKeyNames := feastdevv1alpha1.SecretKeyNames{ + TlsCrt: "tls.crt", + TlsKey: "tls.key", + } + + It("should set default TLS configs", func() { + By("Having the created resource") + + // registry service w/o tls + feast := FeastServices{ + Handler: handler.FeastHandler{ + FeatureStore: minimalFeatureStore(), + Scheme: scheme, + }, + } + err := feast.ApplyDefaults() + Expect(err).To(BeNil()) + + tls := feast.getTlsConfigs(RegistryFeastType) + Expect(tls).To(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + Expect(getPortStr(tls)).To(Equal("80")) + + Expect(feast.offlineTls()).To(BeFalse()) + Expect(feast.remoteRegistryTls()).To(BeFalse()) + Expect(feast.localRegistryTls()).To(BeFalse()) + Expect(feast.isOpenShiftTls(OfflineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(OnlineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(RegistryFeastType)).To(BeFalse()) + openshiftTls, err := feast.checkOpenshiftTls() + Expect(err).To(BeNil()) + Expect(openshiftTls).To(BeFalse()) + + // registry service w/ openshift tls + testSetIsOpenShift() + feast.Handler.FeatureStore = minimalFeatureStore() + err = feast.ApplyDefaults() + Expect(err).To(BeNil()) + + tls = feast.getTlsConfigs(OfflineFeastType) + Expect(tls).To(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + tls = feast.getTlsConfigs(OnlineFeastType) + Expect(tls).To(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + tls = feast.getTlsConfigs(RegistryFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeTrue()) + Expect(tls.SecretKeyNames).To(Equal(secretKeyNames)) + Expect(getPortStr(tls)).To(Equal("443")) + Expect(GetTlsPath(RegistryFeastType)).To(Equal("/tls/registry/")) + + Expect(feast.offlineTls()).To(BeFalse()) + Expect(feast.remoteRegistryTls()).To(BeFalse()) + Expect(feast.localRegistryTls()).To(BeTrue()) + Expect(feast.isOpenShiftTls(OfflineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(OnlineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(RegistryFeastType)).To(BeTrue()) + openshiftTls, err = feast.checkOpenshiftTls() + Expect(err).To(BeNil()) + Expect(openshiftTls).To(BeTrue()) + + // all services w/ openshift tls + feast.Handler.FeatureStore = minimalFeatureStoreWithAllServices() + err = feast.ApplyDefaults() + Expect(err).To(BeNil()) + + repoConfig := getClientRepoConfig(feast.Handler.FeatureStore) + Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) + Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) + Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) + Expect(repoConfig.OnlineStore.Cert).To(ContainSubstring(string(OnlineFeastType))) + Expect(repoConfig.Registry.Cert).To(ContainSubstring(string(RegistryFeastType))) + + tls = feast.getTlsConfigs(OfflineFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeTrue()) + tls = feast.getTlsConfigs(OnlineFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeTrue()) + tls = feast.getTlsConfigs(RegistryFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.SecretKeyNames).To(Equal(secretKeyNames)) + Expect(tls.IsTLS()).To(BeTrue()) + + Expect(feast.offlineTls()).To(BeTrue()) + Expect(feast.remoteRegistryTls()).To(BeFalse()) + Expect(feast.localRegistryTls()).To(BeTrue()) + Expect(feast.isOpenShiftTls(OfflineFeastType)).To(BeTrue()) + Expect(feast.isOpenShiftTls(OnlineFeastType)).To(BeTrue()) + Expect(feast.isOpenShiftTls(RegistryFeastType)).To(BeTrue()) + openshiftTls, err = feast.checkOpenshiftTls() + Expect(err).To(BeNil()) + Expect(openshiftTls).To(BeTrue()) + + // check k8s deployment objects + offlineDeploy := feast.initFeastDeploy(OfflineFeastType) + err = feast.setDeployment(offlineDeploy, OfflineFeastType) + Expect(err).To(BeNil()) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers[0].Command).To(ContainElements(ContainSubstring("-insecure"))) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) + Expect(offlineDeploy.Spec.Template.Spec.Volumes).To(HaveLen(2)) + onlineDeploy := feast.initFeastDeploy(OnlineFeastType) + err = feast.setDeployment(onlineDeploy, OnlineFeastType) + Expect(err).To(BeNil()) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers[0].Command).To(ContainElements(ContainSubstring("-insecure"))) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) + Expect(onlineDeploy.Spec.Template.Spec.Volumes).To(HaveLen(3)) + + // registry service w/ tls and in an openshift cluster + feast.Handler.FeatureStore = minimalFeatureStore() + feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + TLS: &feastdevv1alpha1.TlsConfigs{}, + }, + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + TLS: &feastdevv1alpha1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{}, + SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ + TlsCrt: "test.crt", + }, + }, + }, + }, + } + err = feast.ApplyDefaults() + Expect(err).To(BeNil()) + + tls = feast.getTlsConfigs(OfflineFeastType) + Expect(tls).To(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + tls = feast.getTlsConfigs(OnlineFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + tls = feast.getTlsConfigs(RegistryFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeTrue()) + Expect(tls.SecretKeyNames).NotTo(Equal(secretKeyNames)) + Expect(getPortStr(tls)).To(Equal("443")) + Expect(GetTlsPath(RegistryFeastType)).To(Equal("/tls/registry/")) + + Expect(feast.offlineTls()).To(BeFalse()) + Expect(feast.remoteRegistryTls()).To(BeFalse()) + Expect(feast.localRegistryTls()).To(BeTrue()) + Expect(feast.isOpenShiftTls(OfflineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(OnlineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(RegistryFeastType)).To(BeFalse()) + openshiftTls, err = feast.checkOpenshiftTls() + Expect(err).To(BeNil()) + Expect(openshiftTls).To(BeFalse()) + + // all services w/ tls and in an openshift cluster + feast.Handler.FeatureStore = minimalFeatureStoreWithAllServices() + disable := true + feast.Handler.FeatureStore.Spec.Services.OnlineStore.TLS = &feastdevv1alpha1.TlsConfigs{ + Disable: &disable, + } + feast.Handler.FeatureStore.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + TLS: &feastdevv1alpha1.TlsConfigs{ + Disable: &disable, + }, + }, + } + err = feast.ApplyDefaults() + Expect(err).To(BeNil()) + + repoConfig = getClientRepoConfig(feast.Handler.FeatureStore) + Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) + Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) + Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) + Expect(repoConfig.OnlineStore.Cert).NotTo(ContainSubstring(string(OnlineFeastType))) + Expect(repoConfig.Registry.Cert).NotTo(ContainSubstring(string(RegistryFeastType))) + + tls = feast.getTlsConfigs(OfflineFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeTrue()) + Expect(tls.SecretKeyNames).To(Equal(secretKeyNames)) + tls = feast.getTlsConfigs(OnlineFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + Expect(tls.SecretKeyNames).NotTo(Equal(secretKeyNames)) + tls = feast.getTlsConfigs(RegistryFeastType) + Expect(tls).NotTo(BeNil()) + Expect(tls.IsTLS()).To(BeFalse()) + Expect(tls.SecretKeyNames).NotTo(Equal(secretKeyNames)) + Expect(getPortStr(tls)).To(Equal("80")) + Expect(GetTlsPath(RegistryFeastType)).To(Equal("/tls/registry/")) + + Expect(feast.offlineTls()).To(BeTrue()) + Expect(feast.remoteRegistryTls()).To(BeFalse()) + Expect(feast.localRegistryTls()).To(BeFalse()) + Expect(feast.isOpenShiftTls(OfflineFeastType)).To(BeTrue()) + Expect(feast.isOpenShiftTls(OnlineFeastType)).To(BeFalse()) + Expect(feast.isOpenShiftTls(RegistryFeastType)).To(BeFalse()) + openshiftTls, err = feast.checkOpenshiftTls() + Expect(err).To(BeNil()) + Expect(openshiftTls).To(BeTrue()) + + // check k8s service objects + offlineSvc := feast.initFeastSvc(OfflineFeastType) + Expect(offlineSvc.Annotations).To(BeEmpty()) + err = feast.setService(offlineSvc, OfflineFeastType) + Expect(err).To(BeNil()) + Expect(offlineSvc.Annotations).NotTo(BeEmpty()) + Expect(offlineSvc.Spec.Ports[0].Name).To(Equal(HttpsScheme)) + + onlineSvc := feast.initFeastSvc(OnlineFeastType) + err = feast.setService(onlineSvc, OnlineFeastType) + Expect(err).To(BeNil()) + Expect(onlineSvc.Annotations).To(BeEmpty()) + Expect(onlineSvc.Spec.Ports[0].Name).To(Equal(HttpScheme)) + + // check k8s deployment objects + offlineDeploy = feast.initFeastDeploy(OfflineFeastType) + err = feast.setDeployment(offlineDeploy, OfflineFeastType) + Expect(err).To(BeNil()) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.InitContainers[0].Command).To(ContainElements(ContainSubstring("-plaintext"))) + Expect(offlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(offlineDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) + Expect(offlineDeploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + onlineDeploy = feast.initFeastDeploy(OnlineFeastType) + err = feast.setDeployment(onlineDeploy, OnlineFeastType) + Expect(err).To(BeNil()) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.InitContainers[0].Command).To(ContainElements(ContainSubstring("-plaintext"))) + Expect(onlineDeploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(onlineDeploy.Spec.Template.Spec.Containers[0].Command).NotTo(ContainElements(ContainSubstring("--key"))) + Expect(onlineDeploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 8e6df6ee667..8f871cdd15f 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -13,15 +13,24 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) +var isOpenShift = false + func IsLocalRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { appliedServices := featureStore.Status.Applied.Services return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Local != nil } +func isRemoteRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { + appliedServices := featureStore.Status.Applied.Services + return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Remote != nil +} + func hasPvcConfig(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastServiceType) (*feastdevv1alpha1.PvcConfig, bool) { services := featureStore.Status.Applied.Services var pvcConfig *feastdevv1alpha1.PvcConfig = nil @@ -53,10 +62,6 @@ 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{} } @@ -277,3 +282,32 @@ func CopyMap(original map[string]interface{}) map[string]interface{} { return newCopy } + +// IsOpenShift is a global flag that can be safely called across reconciliation cycles, defined at the controller manager start. +func IsOpenShift() bool { + return isOpenShift +} + +// SetIsOpenShift sets the global flag isOpenShift by the controller manager. +// We don't need to keep fetching the API every reconciliation cycle that we need to know about the platform. +func SetIsOpenShift(cfg *rest.Config) { + if cfg == nil { + panic("Rest Config struct is nil, impossible to get cluster information") + } + // adapted from https://github.com/RHsyseng/operator-utils/blob/a226fabb2226a313dd3a16591c5579ebcd8a74b0/internal/platform/platform_versioner.go#L95 + client, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + panic(fmt.Sprintf("Impossible to get new client for config when fetching cluster information: %s", err)) + } + apiList, err := client.ServerGroups() + if err != nil { + panic(fmt.Sprintf("issue occurred while fetching ServerGroups: %s", err)) + } + + for _, v := range apiList.Groups { + if v.Name == "route.openshift.io" { + isOpenShift = true + break + } + } +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 2819cb24243..b24973ec86c 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -380,8 +380,7 @@ var _ = Describe("FeatureStore API", func() { 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{})) + Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) }) }) }) From 8e1f9402120705f3f7c4bacd8db525e0c9b5a63a Mon Sep 17 00:00:00 2001 From: Matt Green Date: Mon, 2 Dec 2024 17:36:49 -0800 Subject: [PATCH 21/40] chore: Clean up makefile (#4799) * clean up dependency installation makefile tasks Signed-off-by: Matt Green * rewrite lock-python-dependencies-all to be more DRY Signed-off-by: Matt Green * update environment setup docs Signed-off-by: Matt Green * update smoke test Signed-off-by: Matt Green * small change Signed-off-by: Matt Green --------- Signed-off-by: Matt Green Signed-off-by: Theodor Mihalache --- .devcontainer/devcontainer.json | 2 +- .../fork_pr_integration_tests_aws.yml | 4 +- .../fork_pr_integration_tests_gcp.yml | 3 +- .../fork_pr_integration_tests_snowflake.yml | 3 +- .github/workflows/java_master_only.yml | 2 +- .github/workflows/java_pr.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/master_only.yml | 4 +- .github/workflows/nightly-ci.yml | 4 +- .github/workflows/pr_integration_tests.yml | 4 +- .../workflows/pr_local_integration_tests.yml | 2 +- .github/workflows/smoke_tests.yml | 6 +- .github/workflows/unit_tests.yml | 2 +- .gitpod.yml | 2 +- Makefile | 70 ++++++++++--------- .../adding-a-new-offline-store.md | 5 +- docs/project/development-guide.md | 61 +++++++--------- 17 files changed, 84 insertions(+), 94 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1b15dcf882a..4490890d001 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,7 +23,7 @@ // "forwardPorts": [], // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "make install-python-ci-dependencies-uv-venv" + // "postCreateCommand": "make install-python-dependencies-dev" // Configure tool-specific properties. // "customizations": {}, diff --git a/.github/fork_workflows/fork_pr_integration_tests_aws.yml b/.github/fork_workflows/fork_pr_integration_tests_aws.yml index 6eb8b8feff0..d0257ecaca9 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_aws.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_aws.yml @@ -73,7 +73,7 @@ jobs: sudo apt update sudo apt install -y -V libarrow-dev - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -85,5 +85,3 @@ jobs: pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "File and not Snowflake and not BigQuery and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "dynamo and not Snowflake and not BigQuery and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "Redshift and not Snowflake and not BigQuery and not minio_registry" - - diff --git a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml index be9844a7e93..a6221d3b7ac 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml @@ -75,7 +75,7 @@ jobs: sudo apt update sudo apt install -y -V libarrow-dev - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -86,4 +86,3 @@ jobs: run: | pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "BigQuery and not dynamo and not Redshift and not Snowflake and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "File and not dynamo and not Redshift and not Snowflake and not minio_registry" - diff --git a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml index a136b47b9e7..9698fe12cd7 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml @@ -65,7 +65,7 @@ jobs: sudo apt update sudo apt install -y -V libarrow-dev - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -82,4 +82,3 @@ jobs: run: | pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "Snowflake and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "File and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" - diff --git a/.github/workflows/java_master_only.yml b/.github/workflows/java_master_only.yml index 2775f500f32..127b59c5437 100644 --- a/.github/workflows/java_master_only.yml +++ b/.github/workflows/java_master_only.yml @@ -126,7 +126,7 @@ jobs: key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install Python dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - uses: actions/cache@v4 with: path: ~/.m2/repository diff --git a/.github/workflows/java_pr.yml b/.github/workflows/java_pr.yml index caf31ab47fc..8b83646c11f 100644 --- a/.github/workflows/java_pr.yml +++ b/.github/workflows/java_pr.yml @@ -180,7 +180,7 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Run integration tests run: make test-java-integration - name: Save report diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ded9931737a..e3d668b17c5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -19,6 +19,6 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install dependencies run: | - make install-python-ci-dependencies-uv + make install-python-dependencies-ci - name: Lint python run: make lint-python diff --git a/.github/workflows/master_only.yml b/.github/workflows/master_only.yml index 7166246da5f..446c3b1f3be 100644 --- a/.github/workflows/master_only.yml +++ b/.github/workflows/master_only.yml @@ -65,7 +65,7 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -130,4 +130,4 @@ jobs: make push-${{ matrix.component }}-docker REGISTRY=${REGISTRY} VERSION=${GITHUB_SHA} docker tag ${REGISTRY}/${{ matrix.component }}:${GITHUB_SHA} ${REGISTRY}/${{ matrix.component }}:develop - docker push ${REGISTRY}/${{ matrix.component }}:develop \ No newline at end of file + docker push ${REGISTRY}/${{ matrix.component }}:develop diff --git a/.github/workflows/nightly-ci.yml b/.github/workflows/nightly-ci.yml index 11c91af2d7b..886aed44751 100644 --- a/.github/workflows/nightly-ci.yml +++ b/.github/workflows/nightly-ci.yml @@ -141,7 +141,7 @@ jobs: if: matrix.os == 'macos-13' run: brew install apache-arrow - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -154,4 +154,4 @@ jobs: SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }} SNOWFLAKE_CI_ROLE: ${{ secrets.SNOWFLAKE_CI_ROLE }} SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} - run: make test-python-integration \ No newline at end of file + run: make test-python-integration diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index 59de3ce9585..5a1b483b39e 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -86,7 +86,7 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Setup Redis Cluster run: | docker pull vishnunair/docker-redis-cluster:latest @@ -100,4 +100,4 @@ jobs: SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }} SNOWFLAKE_CI_ROLE: ${{ secrets.SNOWFLAKE_CI_ROLE }} SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} - run: make test-python-integration \ No newline at end of file + run: make test-python-integration diff --git a/.github/workflows/pr_local_integration_tests.yml b/.github/workflows/pr_local_integration_tests.yml index 6515d411f01..8b2f8c13d2e 100644 --- a/.github/workflows/pr_local_integration_tests.yml +++ b/.github/workflows/pr_local_integration_tests.yml @@ -48,7 +48,7 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Test local integration tests if: ${{ always() }} # this will guarantee that step won't be canceled and resources won't leak run: make test-python-integration-local diff --git a/.github/workflows/smoke_tests.yml b/.github/workflows/smoke_tests.yml index 782f8b3f511..774d58d22b4 100644 --- a/.github/workflows/smoke_tests.yml +++ b/.github/workflows/smoke_tests.yml @@ -33,6 +33,8 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-dependencies-uv + run: | + uv pip sync --system sdk/python/requirements/py${{ matrix.python-version }}-requirements.txt + uv pip install --system --no-deps . - name: Test Imports - run: python -c "from feast import cli" \ No newline at end of file + run: python -c "from feast import cli" diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index af23c8d808c..a8ddd397e30 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -36,7 +36,7 @@ jobs: path: ${{ steps.uv-cache.outputs.dir }} key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} - name: Install dependencies - run: make install-python-ci-dependencies-uv + run: make install-python-dependencies-ci - name: Test Python run: make test-python-unit diff --git a/.gitpod.yml b/.gitpod.yml index 480baefede4..6e0c28da94d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -8,7 +8,7 @@ tasks: uv pip install pre-commit pre-commit install --hook-type pre-commit --hook-type pre-push source .venv/bin/activate - export PYTHON=3.10 && make install-python-ci-dependencies-uv-venv + export PYTHON=3.10 && make install-python-dependencies-dev # git config --global alias.ci 'commit -s' # git config --global alias.sw switch # git config --global alias.st status diff --git a/Makefile b/Makefile index f155a69a72a..778216616f4 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,12 @@ endif TRINO_VERSION ?= 376 PYTHON_VERSION = ${shell python --version | grep -Eo '[0-9]\.[0-9]+'} +PYTHON_VERSIONS := 3.9 3.10 3.11 +define get_env_name +$(subst .,,py$(1)) +endef + + # General format: format-python format-java @@ -35,50 +41,50 @@ protos: compile-protos-python compile-protos-docs build: protos build-java build-docker -# Python SDK +# Python SDK - local +# formerly install-python-ci-dependencies-uv-venv +# editable install +install-python-dependencies-dev: + uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt + uv pip install --no-deps -e . -install-python-dependencies-uv: - uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt - uv pip install --system --no-deps . +# Python SDK - system +# the --system flag installs dependencies in the global python context +# instead of a venv which is useful when working in a docker container or ci. -install-python-dependencies-uv-venv: - uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt - uv pip install --no-deps . +# Used in github actions/ci +# formerly install-python-ci-dependencies-uv +install-python-dependencies-ci: + uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt + uv pip install --system --no-deps -e . +# Used by multicloud/Dockerfile.dev install-python-ci-dependencies: python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt pip install --no-deps -e . -install-python-ci-dependencies-uv: - uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt - uv pip install --system --no-deps -e . - -install-python-ci-dependencies-uv-venv: - uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt - uv pip install --no-deps -e . - -lock-python-ci-dependencies: - uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt - -compile-protos-python: - python infra/scripts/generate_protos.py - +# Currently used in test-end-to-end.sh install-python: python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt python setup.py develop -lock-python-dependencies: - uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt - lock-python-dependencies-all: - # Remove all existing requirements because we noticed the lock file is not always updated correctly. Removing and running the command again ensures that the lock file is always up to date. - rm -r sdk/python/requirements/* - pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.9 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt" - pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt" - pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.10 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt" - pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt" - pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.11 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt" - pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt" + # Remove all existing requirements because we noticed the lock file is not always updated correctly. + # Removing and running the command again ensures that the lock file is always up to date. + rm -rf sdk/python/requirements/* 2>/dev/null || true + + $(foreach ver,$(PYTHON_VERSIONS),\ + pixi run --environment $(call get_env_name,$(ver)) --manifest-path infra/scripts/pixi/pixi.toml \ + "uv pip compile -p $(ver) --system --no-strip-extras setup.py \ + --output-file sdk/python/requirements/py$(ver)-requirements.txt" && \ + pixi run --environment $(call get_env_name,$(ver)) --manifest-path infra/scripts/pixi/pixi.toml \ + "uv pip compile -p $(ver) --system --no-strip-extras setup.py --extra ci \ + --output-file sdk/python/requirements/py$(ver)-ci-requirements.txt" && \ + ) true + + +compile-protos-python: + python infra/scripts/generate_protos.py benchmark-python: IS_TEST=True python -m pytest --integration --benchmark --benchmark-autosave --benchmark-save-data sdk/python/tests diff --git a/docs/how-to-guides/customizing-feast/adding-a-new-offline-store.md b/docs/how-to-guides/customizing-feast/adding-a-new-offline-store.md index 28592f0cd1a..c8e0258fdf7 100644 --- a/docs/how-to-guides/customizing-feast/adding-a-new-offline-store.md +++ b/docs/how-to-guides/customizing-feast/adding-a-new-offline-store.md @@ -440,11 +440,10 @@ test-python-universal-spark: ### 7. Dependencies -Add any dependencies for your offline store to our `sdk/python/setup.py` under a new `__REQUIRED` list with the packages and add it to the setup script so that if your offline store is needed, users can install the necessary python packages. These packages should be defined as extras so that they are not installed by users by default. You will need to regenerate our requirements files. To do this, create separate pyenv environments for python 3.8, 3.9, and 3.10. In each environment, run the following commands: +Add any dependencies for your offline store to our `sdk/python/setup.py` under a new `__REQUIRED` list with the packages and add it to the setup script so that if your offline store is needed, users can install the necessary python packages. These packages should be defined as extras so that they are not installed by users by default. You will need to regenerate our requirements files: ``` -export PYTHON= -make lock-python-ci-dependencies +make lock-python-ci-dependencies-all ``` ### 8. Add Documentation diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index b6137741906..5b2d0a521e8 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -54,8 +54,8 @@ See [Contribution process](./contributing.md) and [Community](../community.md) f ## Making a pull request We use the convention that the assignee of a PR is the person with the next action. -If the assignee is empty it means that no reviewer has been found yet. -If a reviewer has been found, they should also be the assigned the PR. +If the assignee is empty it means that no reviewer has been found yet. +If a reviewer has been found, they should also be the assigned the PR. Finally, if there are comments to be addressed, the PR author should be the one assigned the PR. PRs that are submitted by the general public need to be identified as `ok-to-test`. Once enabled, [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) will run a range of tests to verify the submission, after which community members will help to review the pull request. @@ -120,51 +120,39 @@ Note that this means if you are midway through working through a PR and rebase, ## Feast Python SDK and CLI ### Environment Setup -Setting up your development environment for Feast Python SDK and CLI: -1. Ensure that you have Docker installed in your environment. Docker is used to provision service dependencies during testing, and build images for feature servers and other components. +#### Tools +- Docker: Docker is used to provision service dependencies during testing, and build images for feature servers and other components. - Please note that we use [Docker with BuiltKit](https://docs.docker.com/develop/develop-images/build_enhancements/). - _Alternatively_ - To use [podman](https://podman.io/) on a Fedora or RHEL machine, follow this [guide](https://github.com/feast-dev/feast/issues/4190) -2. Ensure that you have `make` and Python (3.9 or above) installed. -3. _Recommended:_ Create a virtual environment to isolate development dependencies to be installed - ```sh - # create & activate a virtual environment - python -m venv venv/ - source venv/bin/activate - ``` -4. (M1 Mac only): Follow the [dev guide](https://github.com/feast-dev/feast/issues/2105) -5. Install uv. It is recommended to use uv for managing python dependencies. +- `make` is used to run various scripts +- [uv](https://docs.astral.sh/) for managing python dependencies. [installation instructions](https://docs.astral.sh/uv/getting-started/installation/) +- (M1 Mac only): Follow the [dev guide if you have issues](https://github.com/feast-dev/feast/issues/2105) +- (Optional): Node & Yarn (needed for building the feast UI) +- (Optional): [Pixi](https://pixi.sh/latest/) for recompile python lock files. Only when you make changes to requirements or simply want to update python lock files to reflect latest versioons. + +### Quick start +- create a new virtual env: `uv venv --python 3.11` (Replace the python version with your desired version) +- activate the venv: `source venv/bin/activate` +- Install dependencies `make install-python-dependencies-dev` + +### building the UI ```sh -curl -LsSf https://astral.sh/uv/install.sh | sh -``` -or -```ssh -pip install uv -``` -6. (Optional): Install Node & Yarn. Then run the following to build Feast UI artifacts for use in `feast ui` -``` make build-ui ``` -7. (Optional) install pixi. pixi is necessary to run step 8 for all python versions at once. -```sh -curl -fsSL https://pixi.sh/install.sh | bash -``` -8. (Optional): Recompile python lock files. Only when you make changes to requirements or simply want to update python lock files to reflect latest versioons. -```sh -make lock-python-dependencies-all -``` -9. Install development dependencies for Feast Python SDK and CLI. This will install package versions from the lock file, install editable version of feast and compile protobufs. -If running inside a virtual environment: +### Recompiling python lock files +Recompile python lock files. This only needs to be run when you make changes to requirements or simply want to update python lock files to reflect latest versions. + ```sh -make install-python-ci-dependencies-uv-venv +make lock-python-dependencies-all ``` -Otherwise: +### Building protos ```sh -make install-python-ci-dependencies-uv +make compile-protos-python ``` -10. Spin up Docker Image +### Building a docker image for development ```sh docker build -t docker-whale -f ./sdk/python/feast/infra/feature_servers/multicloud/Dockerfile . ``` @@ -405,7 +393,7 @@ It will: ### Testing with Github Actions workflows -Please refer to the maintainers [doc](maintainers.md) if you would like to locally test out the github actions workflow changes. +Please refer to the maintainers [doc](maintainers.md) if you would like to locally test out the github actions workflow changes. This document will help you setup your fork to test the ci integration tests and other workflows without needing to make a pull request against feast-dev master. ## Feast Data Storage Format @@ -414,4 +402,3 @@ Feast data storage contracts are documented in the following locations: * [Feast Offline Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/offline_store_format.md): Used by BigQuery, Snowflake \(Future\), Redshift \(Future\). * [Feast Online Storage Format](https://github.com/feast-dev/feast/blob/master/docs/specs/online_store_format.md): Used by Redis, Google Datastore. - From 7add6dc89f35e9980c6900259624a1b44cedd1d5 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 09:42:28 -0500 Subject: [PATCH 22/40] Added secret and registry to sample yaml Signed-off-by: Theodor Mihalache --- .../v1alpha1_featurestore_db_persistence.yaml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml index 0540bc90ccf..f99815d7100 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml @@ -11,5 +11,25 @@ spec: store: type: postgres secretRef: - name: my-secret - secretKeyName: mykey # optional + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional + registry: + local: + persistence: + store: + type: sql + secretRef: + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret +stringData: + postgres-secret-parameters: | + path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true \ No newline at end of file From cf25793e5873b59c0a2e6e15f647ee0ba90e872d Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:27:26 -0500 Subject: [PATCH 23/40] - Added missing go operator test file - Added tests for invalid store type for each of the feast service Signed-off-by: Theodor Mihalache --- .../featurestore_controller_db_store_test.go | 713 ++++++++++++++++++ .../test/api/featurestore_types_test.go | 54 ++ 2 files changed, 767 insertions(+) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go new file mode 100644 index 00000000000..5badd4bf19b --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -0,0 +1,713 @@ +/* +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/feast-dev/feast/infra/feast-operator/internal/controller/handler" + + . "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" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "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/services" +) + +var cassandraYamlString = ` +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var snowflakeYamlString = ` +account: snowflake_deployment.us-east-1 +user: user_login +password: user_password +role: SYSADMIN +warehouse: COMPUTE_WH +database: FEAST +schema: PUBLIC +` + +var sqlTypeYamlString = ` +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var invalidSecretTypeYamlString = ` +type: cassandra +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var invalidSecretRegistryTypeYamlString = ` +registry_type: sql +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var _ = Describe("FeatureStore Controller - db storage services", func() { + Context("When deploying a resource with all db storage services", func() { + const resourceName = "cr-name" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + offlineSecretNamespacedName := types.NamespacedName{ + Name: "offline-store-secret", + Namespace: "default", + } + + onlineSecretNamespacedName := types.NamespacedName{ + Name: "online-store-secret", + Namespace: "default", + } + + registrySecretNamespacedName := types.NamespacedName{ + Name: "registry-store-secret", + Namespace: "default", + } + + featurestore := &feastdevv1alpha1.FeatureStore{} + offlineType := services.OfflineDBPersistenceSnowflakeConfigType + onlineType := services.OnlineDBPersistenceCassandraConfigType + registryType := services.RegistryDBPersistenceSQLConfigType + + BeforeEach(func() { + By("creating secrets for db stores for custom resource of Kind FeatureStore") + secret := &corev1.Secret{} + + secretData := map[string][]byte{ + string(offlineType): []byte(snowflakeYamlString), + } + err := k8sClient.Get(ctx, offlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: offlineSecretNamespacedName.Name, + Namespace: offlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + string(onlineType): []byte(cassandraYamlString), + } + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: onlineSecretNamespacedName.Name, + Namespace: onlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + "sql_custom_registry_key": []byte(sqlTypeYamlString), + } + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: registrySecretNamespacedName.Name, + Namespace: registrySecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + 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.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: string(offlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "offline-store-secret", + }, + }, + } + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: string(onlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "online-store-secret", + }, + }, + } + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: string(registryType), + SecretRef: corev1.LocalObjectReference{ + Name: "registry-store-secret", + }, + SecretKeyName: "sql_custom_registry_key", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + onlineSecret := &corev1.Secret{} + err := k8sClient.Get(ctx, onlineSecretNamespacedName, onlineSecret) + Expect(err).NotTo(HaveOccurred()) + + offlineSecret := &corev1.Secret{} + err = k8sClient.Get(ctx, offlineSecretNamespacedName, offlineSecret) + Expect(err).NotTo(HaveOccurred()) + + registrySecret := &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, registrySecret) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the secrets") + Expect(k8sClient.Delete(ctx, onlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, offlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, registrySecret)).To(Succeed()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should fail reconciling the resource", func() { + By("Referring to a secret that doesn't exist") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "invalid_secret"} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secrets \"invalid_secret\" not found")) + + By("Referring to a secret with a key that doesn't exist") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key invalid.secret.key doesn't exist in secret online-store-secret")) + + By("Referring to a secret that contains parameter named type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named registry_type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(cassandraYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data["sql_custom_registry_key"] = nil + secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key sql in secret registry-store-secret contains invalid tag named registry_type")) + }) + + 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)) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) + 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.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + svc := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + }) + + It("should properly encode a feature_store.yaml config", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + dbParametersMap := unmarshallYamlString(sqlTypeYamlString) + copyMap := services.CopyMap(dbParametersMap) + delete(dbParametersMap, "path") + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + Path: copyMap["path"].(string), + RegistryType: services.RegistryDBPersistenceSQLConfigType, + DBParameters: dbParametersMap, + }, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineDBPersistenceSnowflakeConfigType, + DBParameters: unmarshallYamlString(snowflakeYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Type: onlineType, + DBParameters: unmarshallYamlString(cassandraYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + + // 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: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change paths and reconcile + resourceNew := resource.DeepCopy() + newOnlineSecretName := "offline-store-secret" + newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = string(services.OfflineDBPersistenceSnowflakeConfigType) + 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 online config + 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()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + + repoConfigOnline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + onlineConfig.OnlineStore.Type = services.OnlineDBPersistenceSnowflakeConfigType + onlineConfig.OnlineStore.DBParameters = unmarshallYamlString(snowflakeYamlString) + Expect(repoConfigOnline).To(Equal(onlineConfig)) + }) + }) +}) + +func unmarshallYamlString(yamlString string) map[string]interface{} { + var parameters map[string]interface{} + + err := yaml.Unmarshal([]byte(yamlString), ¶meters) + if err != nil { + fmt.Println(err) + } + return parameters +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index b24973ec86c..6f150c60d5b 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -272,6 +272,50 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } +func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + }, + } + return fsCopy +} + const resourceName = "test-resource" const namespaceName = "default" @@ -313,6 +357,10 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("s3://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("gs://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") }) + + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.online\", \"redis\", \"ikv\", \"datastore\", \"dynamodb\", \"bigtable\", \"postgres\", \"cassandra\", \"mysql\", \"hazelcast\", \"singlestore\"") + }) }) Context("When creating an invalid Offline Store", func() { @@ -321,6 +369,9 @@ var _ = Describe("FeatureStore API", func() { It("should fail when PVC persistence has absolute path", func() { attemptInvalidCreationAndAsserts(ctx, offlineStoreWithUnmanagedFileType(featurestore), "Unsupported value") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.offline\", \"bigquery\", \"redshift\", \"spark\", \"postgres\", \"feast_trino.trino.TrinoOfflineStore\", \"redis\"") + }) }) Context("When creating an invalid Registry", func() { @@ -340,6 +391,9 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForFile(featurestore), "Additional S3 settings are available only for S3 object store URIs") attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"sql\", \"snowflake.registry\"") + }) }) Context("When creating an invalid PvcConfig", func() { From ebbfdac06591e8dc99c912b11458dfdc4509fb82 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:43:40 -0500 Subject: [PATCH 24/40] Added a description for SecretKeyName and the default behaviour Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 4bc0aa7c5e0..e5f9d7f14d4 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,9 +108,11 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOfflineStoreDBStorePersistenceTypes = []string{ @@ -149,9 +151,11 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOnlineStoreDBStorePersistenceTypes = []string{ @@ -196,9 +200,11 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidRegistryDBStorePersistenceTypes = []string{ From 22593b97f908d78f3fc06cbb2df5e111a2edee2a Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 12:13:57 -0500 Subject: [PATCH 25/40] Added a test where the secret contains invalid type Signed-off-by: Theodor Mihalache --- .../crd/bases/feast.dev_featurestores.yaml | 18 +++++++ infra/feast-operator/dist/install.yaml | 18 +++++++ .../featurestore_controller_db_store_test.go | 47 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) 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 958c7cdddb1..2c7ce4f0501 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,6 +297,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -652,6 +655,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1025,6 +1031,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1468,6 +1477,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1829,6 +1841,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2210,6 +2225,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b5e103f9692..b83239cec0a 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,6 +305,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -660,6 +663,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1033,6 +1039,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1476,6 +1485,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1837,6 +1849,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2218,6 +2233,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 5badd4bf19b..547edbc5181 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -20,8 +20,8 @@ import ( "context" "encoding/base64" "fmt" - "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" @@ -78,7 +78,7 @@ sqlalchemy_config_kwargs: pool_pre_ping: true ` -var invalidSecretTypeYamlString = ` +var invalidSecretContainingTypeYamlString = ` type: cassandra hosts: - 192.168.1.1 @@ -96,6 +96,24 @@ read_concurrency: 100 write_concurrency: 100 ` +var invalidSecretTypeYamlString = ` +type: wrong +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var invalidSecretRegistryTypeYamlString = ` registry_type: sql path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast @@ -295,6 +313,31 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret := &corev1.Secret{} err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretContainingTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named type with invalid value") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) From de9a5ff56926059c3fa272e90c7b3348985a432f Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:38:24 -0500 Subject: [PATCH 26/40] Updated the description of SecretKeyName and SecretRef parameters in the CRD Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 18 ++--- .../crd/bases/feast.dev_featurestores.yaml | 70 +++++++++---------- infra/feast-operator/dist/install.yaml | 70 +++++++++---------- 3 files changed, 77 insertions(+), 81 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index e5f9d7f14d4..bf59a1d4ba4 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,10 +108,10 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -151,10 +151,10 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -200,10 +200,10 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } 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 2c7ce4f0501..ac193e2adfd 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -297,14 +297,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -655,14 +654,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1031,14 +1029,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1477,14 +1475,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1841,14 +1839,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2225,14 +2223,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b83239cec0a..10fde132941 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -305,14 +305,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -663,14 +662,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1039,14 +1037,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1485,14 +1483,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1849,14 +1847,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2233,14 +2231,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- From 8b632b8932f7debcb02c6f3c9a4a57ef0b7aa99d Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:46:23 -0500 Subject: [PATCH 27/40] Fixed error Signed-off-by: Theodor Mihalache --- .../controller/featurestore_controller_db_store_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 547edbc5181..60235fe687e 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -505,7 +505,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { From eb111d673ee5cea2cfadda55d0917a591cd6c377 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:04:51 +0100 Subject: [PATCH 28/40] feat: OIDC authorization in Feast Operator (#4801) * Initial commit Signed-off-by: Daniele Martinoli * no private image Signed-off-by: Daniele Martinoli * removed nameLabelKey, using serices.NameLabelKey Signed-off-by: Daniele Martinoli * improved CRD comments and using IsLocalRegistry Signed-off-by: Daniele Martinoli * fixing generated code Signed-off-by: Daniele Martinoli * renamed auth condition and types Signed-off-by: Daniele Martinoli * more renamings Signed-off-by: Daniele Martinoli * initial commit Signed-off-by: Daniele Martinoli * oidc IT Signed-off-by: Daniele Martinoli * with sample Signed-off-by: Daniele Martinoli * no private image Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- .../api/v1alpha1/featurestore_types.go | 8 + .../api/v1alpha1/zz_generated.deepcopy.go | 21 + .../crd/bases/feast.dev_featurestores.yaml | 49 ++ .../v1alpha1_featurestore_oidc_auth.yaml | 35 ++ infra/feast-operator/dist/install.yaml | 49 ++ .../internal/controller/authz/authz.go | 14 +- ...restore_controller_kubernetes_auth_test.go | 4 +- .../featurestore_controller_oidc_auth_test.go | 589 ++++++++++++++++++ .../featurestore_controller_test.go | 1 - .../internal/controller/services/client.go | 2 +- .../controller/services/repo_config.go | 119 +++- .../controller/services/repo_config_test.go | 130 ++++ .../controller/services/services_types.go | 18 +- .../internal/controller/services/tls_test.go | 6 +- .../internal/controller/services/util.go | 4 + .../test/api/featurestore_types_test.go | 31 +- 16 files changed, 1035 insertions(+), 45 deletions(-) create mode 100644 infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 4bc0aa7c5e0..bdeecd570d1 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -280,8 +280,10 @@ type OptionalConfigs struct { } // AuthzConfig defines the authorization settings for the deployed Feast services. +// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc)].exists_one(c, c)",message="One selection required between kubernetes or oidc." type AuthzConfig struct { KubernetesAuthz *KubernetesAuthz `json:"kubernetes,omitempty"` + OidcAuthz *OidcAuthz `json:"oidc,omitempty"` } // KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. @@ -296,6 +298,12 @@ type KubernetesAuthz struct { Roles []string `json:"roles,omitempty"` } +// OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. +// https://auth0.com/docs/authenticate/protocols/openid-connect-protocol +type OidcAuthz struct { + SecretRef corev1.LocalObjectReference `json:"secretRef"` +} + // TlsConfigs configures server TLS for a feast service. in an openshift cluster, this is configured by default using service serving certificates. // +kubebuilder:validation:XValidation:rule="(!has(self.disable) || !self.disable) ? has(self.secretRef) : true",message="`secretRef` required if `disable` is false." type TlsConfigs struct { diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 8675397fee5..3f317c650e9 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -34,6 +34,11 @@ func (in *AuthzConfig) DeepCopyInto(out *AuthzConfig) { *out = new(KubernetesAuthz) (*in).DeepCopyInto(*out) } + if in.OidcAuthz != nil { + in, out := &in.OidcAuthz, &out.OidcAuthz + *out = new(OidcAuthz) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthzConfig. @@ -373,6 +378,22 @@ func (in *OfflineTlsConfigs) DeepCopy() *OfflineTlsConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OidcAuthz) DeepCopyInto(out *OidcAuthz) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcAuthz. +func (in *OidcAuthz) DeepCopy() *OidcAuthz { + if in == nil { + return nil + } + out := new(OidcAuthz) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = *in 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 958c7cdddb1..fddac5458fc 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -69,7 +69,31 @@ spec: type: string type: array type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0.com/docs/authenticate/protocols/openid-connect-protocol + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an @@ -1238,7 +1262,32 @@ spec: type: string type: array type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0.com/docs/authenticate/protocols/openid-connect-protocol + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' 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/samples/v1alpha1_featurestore_oidc_auth.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml new file mode 100644 index 00000000000..c70f172ded9 --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml @@ -0,0 +1,35 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: sample-oidc-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: + oidc: + secretRef: + name: oidc-secret +--- +kind: Secret +apiVersion: v1 +metadata: + name: oidc-secret +stringData: + client_id: client_id + auth_discovery_url: auth_discovery_url + client_secret: client_secret + username: username + password: password diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index b5e103f9692..59ca5505b92 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -77,7 +77,31 @@ spec: type: string type: array type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0.com/docs/authenticate/protocols/openid-connect-protocol + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' feastProject: description: FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an @@ -1246,7 +1270,32 @@ spec: type: string type: array type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0.com/docs/authenticate/protocols/openid-connect-protocol + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' 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/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index c7d4b896063..efcae23a4b0 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -18,15 +18,13 @@ import ( // Deploy the feast authorization func (authz *FeastAuthorization) Deploy() error { if authz.isKubernetesAuth() { - if err := authz.deployKubernetesAuth(); err != nil { - return err - } - } else { - authz.removeOrphanedRoles() - _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) - _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) - apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type) + return authz.deployKubernetesAuth() } + + authz.removeOrphanedRoles() + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) + _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) + apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type) return nil } diff --git a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go index 6589e181af7..4930f3fc590 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go @@ -333,9 +333,9 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(err).To(HaveOccurred()) Expect(errors.IsNotFound(err)).To(BeTrue()) - By("Clearing the kubernetes authorizatino and reconciling") + By("Clearing the kubernetes authorization and reconciling") resourceNew = resource.DeepCopy() - resourceNew.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + resourceNew.Spec.AuthzConfig = nil err = k8sClient.Update(ctx, resourceNew) Expect(err).NotTo(HaveOccurred()) _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go new file mode 100644 index 00000000000..c062a573df2 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -0,0 +1,589 @@ +/* +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-OIDC authorization", func() { + Context("When deploying a resource with all ephemeral services and OIDC authorization", func() { + const resourceName = "oidc-authorization" + const oidcSecretName = "oidc-secret" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedSecretName := types.NamespacedName{ + Name: oidcSecretName, + Namespace: "default", + } + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + featurestore := &feastdevv1alpha1.FeatureStore{} + + BeforeEach(func() { + By("creating the OIDC secret") + oidcSecret := createValidOidcSecret(oidcSecretName) + err := k8sClient.Get(ctx, typeNamespacedSecretName, oidcSecret) + if err != nil && errors.IsNotFound(err) { + Expect(k8sClient.Create(ctx, oidcSecret)).To(Succeed()) + } + + 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{OidcAuthz: &feastdevv1alpha1.OidcAuthz{ + SecretRef: corev1.LocalObjectReference{ + Name: oidcSecretName, + }, + }} + + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + oidcSecret := createValidOidcSecret(oidcSecretName) + err = k8sClient.Get(ctx, typeNamespacedSecretName, oidcSecret) + if err != nil && errors.IsNotFound(err) { + By("Cleanup the OIDC secret") + Expect(k8sClient.Delete(ctx, oidcSecret)).To(Succeed()) + } + + 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{ + OidcAuthz: &feastdevv1alpha1.OidcAuthz{ + SecretRef: corev1.LocalObjectReference{ + Name: oidcSecretName, + }, + }, + } + 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).To(BeNil()) + + 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 Feast Role + feastRole := &rbacv1.Role{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: authz.GetFeastRoleName(resource), + Namespace: resource.Namespace, + }, + feastRole) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + // check 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()) + + // check ServiceAccounts + 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()) + } + + By("Clearing the OIDC authorization and reconciling") + resourceNew := resource.DeepCopy() + resourceNew.Spec.AuthzConfig = nil + 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 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: expectedServerOidcAuthorizConfig(), + } + 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: expectedServerOidcAuthorizConfig(), + } + 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: expectedServerOidcAuthorizConfig(), + } + 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: expectedClientOidcAuthorizConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + }) + + It("should fail to reconcile the resource", func() { + By("Reconciling an invalid OIDC set of properties") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + newOidcSecretName := "invalid-secret" + newTypeNamespaceSecretdName := types.NamespacedName{ + Name: newOidcSecretName, + Namespace: "default", + } + newOidcSecret := createInvalidOidcSecret(newOidcSecretName) + err := k8sClient.Get(ctx, newTypeNamespaceSecretdName, newOidcSecret) + if err != nil && errors.IsNotFound(err) { + Expect(k8sClient.Create(ctx, newOidcSecret)).To(Succeed()) + } + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.AuthzConfig.OidcAuthz.SecretRef.Name = newOidcSecretName + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(resource.Status.Conditions).NotTo(BeEmpty()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.FailedReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Message).To(ContainSubstring("missing OIDC")) + }) + }) +}) + +func expectedServerOidcAuthorizConfig() services.AuthzConfig { + return services.AuthzConfig{ + Type: services.OidcAuthType, + OidcParameters: map[string]interface{}{ + string(services.OidcAuthDiscoveryUrl): "auth-discovery-url", + string(services.OidcClientId): "client-id", + }, + } +} +func expectedClientOidcAuthorizConfig() services.AuthzConfig { + return services.AuthzConfig{ + Type: services.OidcAuthType, + OidcParameters: map[string]interface{}{ + string(services.OidcClientSecret): "client-secret", + string(services.OidcUsername): "username", + string(services.OidcPassword): "password"}, + } +} + +func validOidcSecretMap() map[string]string { + return map[string]string{ + string(services.OidcClientId): "client-id", + string(services.OidcAuthDiscoveryUrl): "auth-discovery-url", + string(services.OidcClientSecret): "client-secret", + string(services.OidcUsername): "username", + string(services.OidcPassword): "password", + } +} + +func createValidOidcSecret(secretName string) *corev1.Secret { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "default", + }, + StringData: validOidcSecretMap(), + } + + return secret +} + +func createInvalidOidcSecret(secretName string) *corev1.Secret { + oidcProperties := validOidcSecretMap() + delete(oidcProperties, string(services.OidcClientId)) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "default", + }, + StringData: oidcProperties, + } + + return secret +} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index a6e71934fb0..44c81eca59a 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -964,7 +964,6 @@ var _ = Describe("FeatureStore Controller", func() { }, Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: referencedRegistry.Spec.FeastProject, - AuthzConfig: &feastdevv1alpha1.AuthzConfig{}, Services: &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{}, OfflineStore: &feastdevv1alpha1.OfflineStore{}, diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index 48ca0751cee..d4b78e2611e 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -47,7 +47,7 @@ func (feast *FeastServices) createClientConfigMap() error { func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { cm.Labels = feast.getLabels(ClientFeastType) - clientYaml, err := feast.getClientFeatureStoreYaml() + clientYaml, err := feast.getClientFeatureStoreYaml(feast.extractConfigFromSecret) if err != nil { return err } diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 8b52296160f..22052aa724d 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -47,10 +47,34 @@ func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) (Re 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) { +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) + repoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc) + if err != nil { + return repoConfig, err + } + + if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil { + propertiesMap, err := secretExtractionFunc(appliedSpec.AuthzConfig.OidcAuthz.SecretRef.Name, "") + if err != nil { + return repoConfig, err + } + + oidcServerProperties := map[string]interface{}{} + for _, oidcServerProperty := range OidcServerProperties { + if val, exists := propertiesMap[string(oidcServerProperty)]; exists { + oidcServerProperties[string(oidcServerProperty)] = val + } else { + return repoConfig, missingOidcSecretProperty(oidcServerProperty) + } + } + repoConfig.AuthzConfig.OidcParameters = oidcServerProperties + } + if appliedSpec.Services != nil { services := appliedSpec.Services @@ -200,11 +224,17 @@ func setRepoConfigOffline(services *feastdevv1alpha1.FeatureStoreServices, secre return nil } -func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { - return yaml.Marshal(getClientRepoConfig(feast.Handler.FeatureStore)) +func (feast *FeastServices) getClientFeatureStoreYaml(secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) ([]byte, error) { + clientRepo, err := getClientRepoConfig(feast.Handler.FeatureStore, secretExtractionFunc) + if err != nil { + return []byte{}, err + } + return yaml.Marshal(clientRepo) } -func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig { +func getClientRepoConfig( + featureStore *feastdevv1alpha1.FeatureStore, + secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { status := featureStore.Status appliedServices := status.Applied.Services clientRepoConfig := RepoConfig{ @@ -248,13 +278,37 @@ func getClientRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig } } - clientRepoConfig.AuthzConfig = AuthzConfig{ - Type: NoAuthAuthType, - } - if status.Applied.AuthzConfig != nil && status.Applied.AuthzConfig.KubernetesAuthz != nil { - clientRepoConfig.AuthzConfig.Type = KubernetesAuthType + if status.Applied.AuthzConfig == nil { + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: NoAuthAuthType, + } + } else { + if status.Applied.AuthzConfig.KubernetesAuthz != nil { + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: KubernetesAuthType, + } + } else if status.Applied.AuthzConfig.OidcAuthz != nil { + clientRepoConfig.AuthzConfig = AuthzConfig{ + Type: OidcAuthType, + } + + propertiesMap, err := secretExtractionFunc(status.Applied.AuthzConfig.OidcAuthz.SecretRef.Name, "") + if err != nil { + return clientRepoConfig, err + } + + oidcClientProperties := map[string]interface{}{} + for _, oidcClientProperty := range OidcClientProperties { + if val, exists := propertiesMap[string(oidcClientProperty)]; exists { + oidcClientProperties[string(oidcClientProperty)] = val + } else { + return clientRepoConfig, missingOidcSecretProperty(oidcClientProperty) + } + } + clientRepoConfig.AuthzConfig.OidcParameters = oidcClientProperties + } } - return clientRepoConfig + return clientRepoConfig, nil } func getActualPath(filePath string, pvcConfig *feastdevv1alpha1.PvcConfig) string { @@ -271,24 +325,33 @@ func (feast *FeastServices) extractConfigFromSecret(secretRef string, secretKeyN } parameters := map[string]interface{}{} - val, exists := secret.Data[secretKeyName] - if !exists { - return nil, fmt.Errorf("secret key %s doesn't exist in secret %s", secretKeyName, secretRef) - } - - err = yaml.Unmarshal(val, ¶meters) - if err != nil { - return nil, fmt.Errorf("secret %s contains invalid value", secretKeyName) - } - - _, exists = parameters["type"] - if exists { - return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named type", secretKeyName, secretRef) - } + if secretKeyName != "" { + val, exists := secret.Data[secretKeyName] + if !exists { + return nil, fmt.Errorf("secret key %s doesn't exist in secret %s", secretKeyName, secretRef) + } + err = yaml.Unmarshal(val, ¶meters) + if err != nil { + return nil, fmt.Errorf("secret %s contains invalid value", secretKeyName) + } + _, exists = parameters["type"] + if exists { + return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named type", secretKeyName, secretRef) + } - _, exists = parameters["registry_type"] - if exists { - return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named registry_type", secretKeyName, secretRef) + _, exists = parameters["registry_type"] + if exists { + return nil, fmt.Errorf("secret key %s in secret %s contains invalid tag named registry_type", secretKeyName, secretRef) + } + } else { + for k, v := range secret.Data { + var val interface{} + err := yaml.Unmarshal(v, &val) + if err != nil { + return nil, fmt.Errorf("secret %s contains invalid value %v", k, v) + } + parameters[k] = val + } } return parameters, nil 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 18cf104eb52..cb3f96c7ba7 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -252,6 +252,71 @@ var _ = Describe("Repo Config", func() { } Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + By("Having oidc authorization") + featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ + OidcAuthz: &feastdevv1alpha1.OidcAuthz{ + SecretRef: corev1.LocalObjectReference{ + Name: "oidc-secret", + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcAuthDiscoveryUrl): "discovery-url", + string(OidcClientId): "client-id", + string(OidcClientSecret): "client-secret", + string(OidcUsername): "username", + string(OidcPassword): "password"}) + repoConfig, err = getServiceRepoConfig(OfflineFeastType, featureStore, secretExtractionFunc) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(2)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) + 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, secretExtractionFunc) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(2)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) + 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, secretExtractionFunc) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(2)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) + Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig())) + Expect(repoConfig.OnlineStore).To(Equal(emptyOnlineStoreConfig())) + expectedRegistryConfig = RegistryConfig{ + RegistryType: "file", + Path: DefaultRegistryEphemeralPath, + } + Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + + repoConfig, err = getClientRepoConfig(featureStore, secretExtractionFunc) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(3)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + By("Having the all the db services") featureStore = minimalFeatureStore() featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ @@ -329,6 +394,64 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) }) }) + It("should fail to create the repo configs", func() { + featureStore := minimalFeatureStore() + + By("Having invalid server oidc authorization") + featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ + OidcAuthz: &feastdevv1alpha1.OidcAuthz{ + SecretRef: corev1.LocalObjectReference{ + Name: "oidc-secret", + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcClientId): "client-id", + string(OidcClientSecret): "client-secret", + string(OidcUsername): "username", + string(OidcPassword): "password"}) + _, err := getServiceRepoConfig(OfflineFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(OnlineFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(RegistryFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getClientRepoConfig(featureStore, secretExtractionFunc) + Expect(err).ToNot(HaveOccurred()) + + By("Having invalid client oidc authorization") + featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ + OidcAuthz: &feastdevv1alpha1.OidcAuthz{ + SecretRef: corev1.LocalObjectReference{ + Name: "oidc-secret", + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcAuthDiscoveryUrl): "discovery-url", + string(OidcClientId): "client-id", + string(OidcUsername): "username", + string(OidcPassword): "password"}) + _, err = getServiceRepoConfig(OfflineFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(OnlineFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(RegistryFeastType, featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getClientRepoConfig(featureStore, secretExtractionFunc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + }) }) func emptyOnlineStoreConfig() OnlineStoreConfig { @@ -369,6 +492,13 @@ func mockExtractConfigFromSecret(secretRef string, secretKeyName string) (map[st return createParameterMap(), nil } +func mockOidcConfigFromSecret( + oidcProperties map[string]interface{}) func(secretRef string, secretKeyName string) (map[string]interface{}, error) { + return func(secretRef string, secretKeyName string) (map[string]interface{}, error) { + return oidcProperties, nil + } +} + func createParameterMap() map[string]interface{} { yamlString := ` hosts: diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 65e50b36818..2c454459d88 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -68,6 +68,15 @@ const ( NoAuthAuthType AuthzType = "no_auth" KubernetesAuthType AuthzType = "kubernetes" + OidcAuthType AuthzType = "oidc" + + OidcClientId OidcPropertyType = "client_id" + OidcAuthDiscoveryUrl OidcPropertyType = "auth_discovery_url" + OidcClientSecret OidcPropertyType = "client_secret" + OidcUsername OidcPropertyType = "username" + OidcPassword OidcPropertyType = "password" + + OidcMissingSecretError string = "missing OIDC secret: %s" ) var ( @@ -148,11 +157,17 @@ var ( }, }, } + + OidcServerProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl} + OidcClientProperties = []OidcPropertyType{OidcClientSecret, OidcUsername, OidcPassword} ) // AuthzType defines the authorization type type AuthzType string +// OidcPropertyType defines the OIDC property type +type OidcPropertyType string + // FeastServiceType is the type of feast service type FeastServiceType string @@ -214,7 +229,8 @@ type RegistryConfig struct { // AuthzConfig is the RBAC authorization configuration. type AuthzConfig struct { - Type AuthzType `yaml:"type,omitempty"` + Type AuthzType `yaml:"type,omitempty"` + OidcParameters map[string]interface{} `yaml:",inline,omitempty"` } type deploymentSettings struct { diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index 28a3a49ec2f..a0a6cbe3101 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -102,7 +102,8 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).To(BeNil()) - repoConfig := getClientRepoConfig(feast.Handler.FeatureStore) + repoConfig, err := getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) @@ -208,7 +209,8 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).To(BeNil()) - repoConfig = getClientRepoConfig(feast.Handler.FeatureStore) + repoConfig, err = getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret) + Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 8f871cdd15f..323f87119e3 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -311,3 +311,7 @@ func SetIsOpenShift(cfg *rest.Config) { } } } + +func missingOidcSecretProperty(property OidcPropertyType) error { + return fmt.Errorf(OidcMissingSecretError, property) +} diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index b24973ec86c..0a7f8fd53a2 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -16,7 +16,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// Function to create invalid OnlineStore resource func createFeatureStore() *feastdevv1alpha1.FeatureStore { return &feastdevv1alpha1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ @@ -272,6 +271,25 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } +func authzConfigWithKubernetes(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + if fsCopy.Spec.AuthzConfig == nil { + fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + } + fsCopy.Spec.AuthzConfig.KubernetesAuthz = &feastdevv1alpha1.KubernetesAuthz{ + Roles: []string{}, + } + return fsCopy +} +func authzConfigWithOidc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + if fsCopy.Spec.AuthzConfig == nil { + fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + } + fsCopy.Spec.AuthzConfig.OidcAuthz = &feastdevv1alpha1.OidcAuthz{} + return fsCopy +} + const resourceName = "test-resource" const namespaceName = "default" @@ -377,10 +395,19 @@ 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() { + }) + Context("When omitting the AuthzConfig PvcConfig", func() { + _, featurestore := initContext() + It("should keep an empty AuthzConfig", func() { resource := featurestore services.ApplyDefaultsToStatus(resource) Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) }) }) + Context("When configuring the AuthzConfig", func() { + ctx, featurestore := initContext() + It("should fail when both kubernetes and oidc settings are given", func() { + attemptInvalidCreationAndAsserts(ctx, authzConfigWithOidc(authzConfigWithKubernetes(featurestore)), "One selection required between kubernetes or oidc") + }) + }) }) From 1115d966df8ecff5553ae0c0879559f9ad735245 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Wed, 4 Dec 2024 08:00:37 -0600 Subject: [PATCH 29/40] fix: Operator envVar positioning & tls.SecretRef.Name (#4806) * fix envVar positioning Signed-off-by: Tommy Hughes * tls.SecretRef.Name Signed-off-by: Tommy Hughes --------- Signed-off-by: Tommy Hughes --- .../controller/services/repo_config_test.go | 2 ++ .../internal/controller/services/services.go | 24 +------------------ .../internal/controller/services/tls.go | 24 ++++++++++++------- .../internal/controller/services/tls_test.go | 6 +++++ .../internal/controller/services/util.go | 23 ++++++++++++++++++ 5 files changed, 47 insertions(+), 32 deletions(-) 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 cb3f96c7ba7..b148f904706 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -23,6 +23,7 @@ import ( . "github.com/onsi/gomega" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" ) @@ -468,6 +469,7 @@ func emptyRegistryConfig() RegistryConfig { func minimalFeatureStore() *feastdevv1alpha1.FeatureStore { return &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: projectName, }, diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index c360581cb6e..b1878ee00ae 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -659,7 +659,7 @@ func (feast *FeastServices) initPVC(feastType FeastServiceType) *corev1.Persiste func applyOptionalContainerConfigs(container *corev1.Container, optionalConfigs feastdevv1alpha1.OptionalConfigs) { if optionalConfigs.Env != nil { - container.Env = mergeEnvVarsArrays(container.Env, optionalConfigs.Env) + container.Env = envOverride(container.Env, *optionalConfigs.Env) } if optionalConfigs.ImagePullPolicy != nil { container.ImagePullPolicy = *optionalConfigs.ImagePullPolicy @@ -669,28 +669,6 @@ func applyOptionalContainerConfigs(container *corev1.Container, optionalConfigs } } -func mergeEnvVarsArrays(envVars1 []corev1.EnvVar, envVars2 *[]corev1.EnvVar) []corev1.EnvVar { - merged := make(map[string]corev1.EnvVar) - - // Add all env vars from the first array - for _, envVar := range envVars1 { - merged[envVar.Name] = envVar - } - - // Add all env vars from the second array, overriding duplicates - for _, envVar := range *envVars2 { - merged[envVar.Name] = envVar - } - - // Convert the map back to an array - result := make([]corev1.EnvVar, 0, len(merged)) - for _, envVar := range merged { - result = append(result, envVar) - } - - return result -} - func mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *feastdevv1alpha1.PvcConfig, deployName string) { if podSpec != nil && pvcConfig != nil { container := &podSpec.Containers[0] diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go index 9cab14faa7d..c92c4d8de23 100644 --- a/infra/feast-operator/internal/controller/services/tls.go +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -42,22 +42,28 @@ func (feast *FeastServices) setTlsDefaults() error { func (feast *FeastServices) setOpenshiftTls() error { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services - tlsConfigs := &feastdevv1alpha1.TlsConfigs{ - SecretRef: &corev1.LocalObjectReference{}, - } if feast.offlineOpenshiftTls() { appliedServices.OfflineStore.TLS = &feastdevv1alpha1.OfflineTlsConfigs{ - TlsConfigs: *tlsConfigs, + TlsConfigs: feastdevv1alpha1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: feast.initFeastSvc(OfflineFeastType).Name + tlsNameSuffix, + }, + }, } - appliedServices.OfflineStore.TLS.TlsConfigs.SecretRef.Name = feast.initFeastSvc(OfflineFeastType).Name + tlsNameSuffix } if feast.onlineOpenshiftTls() { - appliedServices.OnlineStore.TLS = tlsConfigs - appliedServices.OnlineStore.TLS.SecretRef.Name = feast.initFeastSvc(OnlineFeastType).Name + tlsNameSuffix + appliedServices.OnlineStore.TLS = &feastdevv1alpha1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: feast.initFeastSvc(OnlineFeastType).Name + tlsNameSuffix, + }, + } } if feast.localRegistryOpenshiftTls() { - appliedServices.Registry.Local.TLS = tlsConfigs - appliedServices.Registry.Local.TLS.SecretRef.Name = feast.initFeastSvc(RegistryFeastType).Name + tlsNameSuffix + appliedServices.Registry.Local.TLS = &feastdevv1alpha1.TlsConfigs{ + SecretRef: &corev1.LocalObjectReference{ + Name: feast.initFeastSvc(RegistryFeastType).Name + tlsNameSuffix, + }, + } } else if remote, err := feast.remoteRegistryOpenshiftTls(); remote { // if the remote registry reference is using openshift's service serving certificates, we can use the injected service CA bundle configMap if appliedServices.Registry.Remote.TLS == nil { diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index a0a6cbe3101..2a66d8a4fdd 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -113,11 +113,17 @@ var _ = Describe("TLS Config", func() { tls = feast.getTlsConfigs(OfflineFeastType) Expect(tls).NotTo(BeNil()) Expect(tls.IsTLS()).To(BeTrue()) + Expect(tls.SecretRef).NotTo(BeNil()) + Expect(tls.SecretRef.Name).To(Equal("feast-test-offline-tls")) tls = feast.getTlsConfigs(OnlineFeastType) Expect(tls).NotTo(BeNil()) Expect(tls.IsTLS()).To(BeTrue()) + Expect(tls.SecretRef).NotTo(BeNil()) + Expect(tls.SecretRef.Name).To(Equal("feast-test-online-tls")) tls = feast.getTlsConfigs(RegistryFeastType) Expect(tls).NotTo(BeNil()) + Expect(tls.SecretRef).NotTo(BeNil()) + Expect(tls.SecretRef.Name).To(Equal("feast-test-registry-tls")) Expect(tls.SecretKeyNames).To(Equal(secretKeyNames)) Expect(tls.IsTLS()).To(BeTrue()) diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 323f87119e3..85bd02e653a 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -315,3 +315,26 @@ func SetIsOpenShift(cfg *rest.Config) { func missingOidcSecretProperty(property OidcPropertyType) error { return fmt.Errorf(OidcMissingSecretError, property) } + +// getEnvVar returns the position of the EnvVar found by name +func getEnvVar(envName string, env []corev1.EnvVar) int { + for pos, v := range env { + if v.Name == envName { + return pos + } + } + return -1 +} + +// envOverride replaces or appends the provided EnvVar to the collection +func envOverride(dst, src []corev1.EnvVar) []corev1.EnvVar { + for _, cre := range src { + pos := getEnvVar(cre.Name, dst) + if pos != -1 { + dst[pos] = cre + } else { + dst = append(dst, cre) + } + } + return dst +} From 6c1fa7b8c01eb68290d2c2da9c27e55590d8c0d4 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:32:06 -0500 Subject: [PATCH 30/40] feat: Added feast Go operator db stores support (#4771) * Add support for db stores in feast go operator Signed-off-by: Theodor Mihalache * Added CR example for store persistence Signed-off-by: Theodor Mihalache * Fixed incorrect yaml tag in RegistryConfig struct Signed-off-by: Theodor Mihalache * Removed leftovers comments from hasAttrib function Signed-off-by: Theodor Mihalache * Added another check that object parameter type is the same as value type in hasAttrib Signed-off-by: Theodor Mihalache * Reverted latest commit Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- .../feast-operator/internal/controller/services/repo_config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 22052aa724d..0526220af5a 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -49,8 +49,7 @@ func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) (Re func getServiceRepoConfig( feastType FeastServiceType, - featureStore *feastdevv1alpha1.FeatureStore, - secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { + featureStore *feastdevv1alpha1.FeatureStore, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { appliedSpec := featureStore.Status.Applied repoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc) From 8252bd3127001b4a7597d5d52a43f27fee86f425 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:14:59 +0100 Subject: [PATCH 31/40] feat: RBAC Authorization in Feast Operator (#4786) * Initial commit Signed-off-by: Daniele Martinoli * refactoring types with FeastHandler Signed-off-by: Daniele Martinoli * no private image Signed-off-by: Daniele Martinoli * removed log-level Signed-off-by: Daniele Martinoli * no empty list for default Role Signed-off-by: Daniele Martinoli * removed nameLabelKey, using serices.NameLabelKey Signed-off-by: Daniele Martinoli * improved CRD comments and using IsLocalRegistry Signed-off-by: Daniele Martinoli * fixing generated code Signed-off-by: Daniele Martinoli * renamed auth condition and types Signed-off-by: Daniele Martinoli * post rebase fixes Signed-off-by: Daniele Martinoli * more renamings Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli Signed-off-by: Theodor Mihalache --- infra/feast-operator/test/api/featurestore_types_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 0a7f8fd53a2..224129857bd 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -395,6 +395,12 @@ 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{})) + }) }) Context("When omitting the AuthzConfig PvcConfig", func() { _, featurestore := initContext() From 89842fa23cc03288a33940262a19158d9bcdc72f Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Mon, 2 Dec 2024 14:18:46 -0600 Subject: [PATCH 32/40] feat: Add TLS support to the Operator (#4796) * add tls support to the operator Signed-off-by: Tommy Hughes * operator tls review fix: if statement Signed-off-by: Tommy Hughes * rebase fixes Signed-off-by: Tommy Hughes * authz rbac fixes Signed-off-by: Tommy Hughes --------- Signed-off-by: Tommy Hughes Signed-off-by: Theodor Mihalache --- .../internal/controller/featurestore_controller_test.go | 1 + infra/feast-operator/test/api/featurestore_types_test.go | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 44c81eca59a..a6e71934fb0 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -964,6 +964,7 @@ var _ = Describe("FeatureStore Controller", func() { }, Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: referencedRegistry.Spec.FeastProject, + AuthzConfig: &feastdevv1alpha1.AuthzConfig{}, Services: &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{}, OfflineStore: &feastdevv1alpha1.OfflineStore{}, diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 224129857bd..a5297111f23 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -398,8 +398,7 @@ var _ = Describe("FeatureStore API", func() { 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{})) + Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) }) }) Context("When omitting the AuthzConfig PvcConfig", func() { From c9a0de5654000779b34f7fb697cd94d1c3ac69a7 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 09:42:28 -0500 Subject: [PATCH 33/40] Added secret and registry to sample yaml Signed-off-by: Theodor Mihalache --- .../v1alpha1_featurestore_db_persistence.yaml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml index 0540bc90ccf..f99815d7100 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml @@ -11,5 +11,25 @@ spec: store: type: postgres secretRef: - name: my-secret - secretKeyName: mykey # optional + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional + registry: + local: + persistence: + store: + type: sql + secretRef: + name: postgres-secret + secretKeyName: postgres-secret-parameters # optional +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret +stringData: + postgres-secret-parameters: | + path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true \ No newline at end of file From cb9c299023c51a7a0751641b44e0f907054c8357 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:27:26 -0500 Subject: [PATCH 34/40] - Added missing go operator test file - Added tests for invalid store type for each of the feast service Signed-off-by: Theodor Mihalache --- .../featurestore_controller_db_store_test.go | 713 ++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go new file mode 100644 index 00000000000..5badd4bf19b --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -0,0 +1,713 @@ +/* +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/feast-dev/feast/infra/feast-operator/internal/controller/handler" + + . "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" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "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/services" +) + +var cassandraYamlString = ` +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var snowflakeYamlString = ` +account: snowflake_deployment.us-east-1 +user: user_login +password: user_password +role: SYSADMIN +warehouse: COMPUTE_WH +database: FEAST +schema: PUBLIC +` + +var sqlTypeYamlString = ` +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var invalidSecretTypeYamlString = ` +type: cassandra +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + +var invalidSecretRegistryTypeYamlString = ` +registry_type: sql +path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast +cache_ttl_seconds: 60 +sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +` + +var _ = Describe("FeatureStore Controller - db storage services", func() { + Context("When deploying a resource with all db storage services", func() { + const resourceName = "cr-name" + var pullPolicy = corev1.PullAlways + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + offlineSecretNamespacedName := types.NamespacedName{ + Name: "offline-store-secret", + Namespace: "default", + } + + onlineSecretNamespacedName := types.NamespacedName{ + Name: "online-store-secret", + Namespace: "default", + } + + registrySecretNamespacedName := types.NamespacedName{ + Name: "registry-store-secret", + Namespace: "default", + } + + featurestore := &feastdevv1alpha1.FeatureStore{} + offlineType := services.OfflineDBPersistenceSnowflakeConfigType + onlineType := services.OnlineDBPersistenceCassandraConfigType + registryType := services.RegistryDBPersistenceSQLConfigType + + BeforeEach(func() { + By("creating secrets for db stores for custom resource of Kind FeatureStore") + secret := &corev1.Secret{} + + secretData := map[string][]byte{ + string(offlineType): []byte(snowflakeYamlString), + } + err := k8sClient.Get(ctx, offlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: offlineSecretNamespacedName.Name, + Namespace: offlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + string(onlineType): []byte(cassandraYamlString), + } + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: onlineSecretNamespacedName.Name, + Namespace: onlineSecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + secret = &corev1.Secret{} + + secretData = map[string][]byte{ + "sql_custom_registry_key": []byte(sqlTypeYamlString), + } + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + if err != nil && errors.IsNotFound(err) { + secret.ObjectMeta = metav1.ObjectMeta{ + Name: registrySecretNamespacedName.Name, + Namespace: registrySecretNamespacedName.Namespace, + } + secret.Data = secretData + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + } + + 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.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: string(offlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "offline-store-secret", + }, + }, + } + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: string(onlineType), + SecretRef: corev1.LocalObjectReference{ + Name: "online-store-secret", + }, + }, + } + resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: string(registryType), + SecretRef: corev1.LocalObjectReference{ + Name: "registry-store-secret", + }, + SecretKeyName: "sql_custom_registry_key", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + onlineSecret := &corev1.Secret{} + err := k8sClient.Get(ctx, onlineSecretNamespacedName, onlineSecret) + Expect(err).NotTo(HaveOccurred()) + + offlineSecret := &corev1.Secret{} + err = k8sClient.Get(ctx, offlineSecretNamespacedName, offlineSecret) + Expect(err).NotTo(HaveOccurred()) + + registrySecret := &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, registrySecret) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the secrets") + Expect(k8sClient.Delete(ctx, onlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, offlineSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, registrySecret)).To(Succeed()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should fail reconciling the resource", func() { + By("Referring to a secret that doesn't exist") + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "invalid_secret"} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secrets \"invalid_secret\" not found")) + + By("Referring to a secret with a key that doesn't exist") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key invalid.secret.key doesn't exist in secret online-store-secret")) + + By("Referring to a secret that contains parameter named type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named registry_type") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(cassandraYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, registrySecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) + secret.Data["sql_custom_registry_key"] = nil + secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key sql in secret registry-store-secret contains invalid tag named registry_type")) + }) + + 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)) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + 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.DBPersistence).NotTo(BeNil()) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) + 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.RegistryReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Replicas).To(Equal(&services.DefaultReplicas)) + Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + + svc := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + svc) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + }) + + It("should properly encode a feature_store.yaml config", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + req, err := labels.NewRequirement(services.NameLabelKey, selection.Equals, []string{resource.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector := labels.NewSelector().Add(*req) + listOpts := &client.ListOptions{Namespace: resource.Namespace, LabelSelector: labelSelector} + deployList := appsv1.DeploymentList{} + err = k8sClient.List(ctx, &deployList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(deployList.Items).To(HaveLen(3)) + + svcList := corev1.ServiceList{} + err = k8sClient.List(ctx, &svcList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(svcList.Items).To(HaveLen(3)) + + cmList := corev1.ConfigMapList{} + err = k8sClient.List(ctx, &cmList, listOpts) + Expect(err).NotTo(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(1)) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + // check registry config + deploy := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.RegistryFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env := getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err := feast.GetServiceFeatureStoreYamlBase64(services.RegistryFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + dbParametersMap := unmarshallYamlString(sqlTypeYamlString) + copyMap := services.CopyMap(dbParametersMap) + delete(dbParametersMap, "path") + testConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + Registry: services.RegistryConfig{ + Path: copyMap["path"].(string), + RegistryType: services.RegistryDBPersistenceSQLConfigType, + DBParameters: dbParametersMap, + }, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfig).To(Equal(testConfig)) + + // check offline config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OfflineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OfflineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOffline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOffline) + Expect(err).NotTo(HaveOccurred()) + regRemote := services.RegistryConfig{ + RegistryType: services.RegistryRemoteConfigType, + Path: fmt.Sprintf("feast-%s-registry.default.svc.cluster.local:80", resourceName), + } + offlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: services.OfflineStoreConfig{ + Type: services.OfflineDBPersistenceSnowflakeConfigType, + DBParameters: unmarshallYamlString(snowflakeYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOffline).To(Equal(offlineConfig)) + + // check online config + deploy = &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: feast.GetFeastServiceName(services.OnlineFeastType), + Namespace: resource.Namespace, + }, + deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) + env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) + Expect(env).NotTo(BeNil()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfigOnline := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + offlineRemote := services.OfflineStoreConfig{ + Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), + Type: services.OfflineRemoteConfigType, + Port: services.HttpPort, + } + onlineConfig := &services.RepoConfig{ + Project: feastProject, + Provider: services.LocalProviderType, + EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + OfflineStore: offlineRemote, + OnlineStore: services.OnlineStoreConfig{ + Type: onlineType, + DBParameters: unmarshallYamlString(cassandraYamlString), + }, + Registry: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigOnline).To(Equal(onlineConfig)) + Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) + + // 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: regRemote, + AuthzConfig: noAuthzConfig(), + } + Expect(repoConfigClient).To(Equal(clientConfig)) + + // change paths and reconcile + resourceNew := resource.DeepCopy() + newOnlineSecretName := "offline-store-secret" + newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} + resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = string(services.OfflineDBPersistenceSnowflakeConfigType) + 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 online config + 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()) + + fsYamlStr, err = feast.GetServiceFeatureStoreYamlBase64(services.OnlineFeastType) + Expect(err).NotTo(HaveOccurred()) + Expect(fsYamlStr).To(Equal(env.Value)) + + envByte, err = base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + + repoConfigOnline = &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfigOnline) + Expect(err).NotTo(HaveOccurred()) + onlineConfig.OnlineStore.Type = services.OnlineDBPersistenceSnowflakeConfigType + onlineConfig.OnlineStore.DBParameters = unmarshallYamlString(snowflakeYamlString) + Expect(repoConfigOnline).To(Equal(onlineConfig)) + }) + }) +}) + +func unmarshallYamlString(yamlString string) map[string]interface{} { + var parameters map[string]interface{} + + err := yaml.Unmarshal([]byte(yamlString), ¶meters) + if err != nil { + fmt.Println(err) + } + return parameters +} From c6bfe716d7dcdcbfe497085e9207bb894aa4c59a Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 11:43:40 -0500 Subject: [PATCH 35/40] Added a description for SecretKeyName and the default behaviour Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index bdeecd570d1..9e15a28b671 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,9 +108,11 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOfflineStoreDBStorePersistenceTypes = []string{ @@ -149,9 +151,11 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidOnlineStoreDBStorePersistenceTypes = []string{ @@ -196,9 +200,11 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` - SecretRef corev1.LocalObjectReference `json:"secretRef"` - SecretKeyName string `json:"secretKeyName,omitempty"` + Type string `json:"type"` + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + SecretKeyName string `json:"secretKeyName,omitempty"` } var ValidRegistryDBStorePersistenceTypes = []string{ From e86f89447a0984b7ff367d819a93d513e73e8bd1 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Mon, 2 Dec 2024 12:13:57 -0500 Subject: [PATCH 36/40] Added a test where the secret contains invalid type Signed-off-by: Theodor Mihalache --- .../crd/bases/feast.dev_featurestores.yaml | 18 +++++++ infra/feast-operator/dist/install.yaml | 18 +++++++ .../featurestore_controller_db_store_test.go | 47 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) 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 fddac5458fc..ce4f36a29d4 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -321,6 +321,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -676,6 +679,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1049,6 +1055,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1517,6 +1526,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1878,6 +1890,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2259,6 +2274,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 59ca5505b92..eedbec8a187 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -329,6 +329,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -684,6 +687,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1057,6 +1063,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1525,6 +1534,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -1886,6 +1898,9 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- @@ -2267,6 +2282,9 @@ spec: the DB store persistence for the registry service properties: secretKeyName: + description: |- + By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if + SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key type: string secretRef: description: |- diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 5badd4bf19b..547edbc5181 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -20,8 +20,8 @@ import ( "context" "encoding/base64" "fmt" - "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" @@ -78,7 +78,7 @@ sqlalchemy_config_kwargs: pool_pre_ping: true ` -var invalidSecretTypeYamlString = ` +var invalidSecretContainingTypeYamlString = ` type: cassandra hosts: - 192.168.1.1 @@ -96,6 +96,24 @@ read_concurrency: 100 write_concurrency: 100 ` +var invalidSecretTypeYamlString = ` +type: wrong +hosts: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 +keyspace: KeyspaceName +port: 9042 +username: user +password: secret +protocol_version: 5 +load_balancing: + local_dc: datacenter1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) +read_concurrency: 100 +write_concurrency: 100 +` + var invalidSecretRegistryTypeYamlString = ` registry_type: sql path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast @@ -295,6 +313,31 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret := &corev1.Secret{} err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) Expect(err).NotTo(HaveOccurred()) + secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretContainingTypeYamlString) + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).To(HaveOccurred()) + + Expect(err.Error()).To(Equal("secret key cassandra in secret online-store-secret contains invalid tag named type")) + + By("Referring to a secret that contains parameter named type with invalid value") + resource = &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + secret = &corev1.Secret{} + err = k8sClient.Get(ctx, onlineSecretNamespacedName, secret) + Expect(err).NotTo(HaveOccurred()) secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) From f6cd4dfd99bb6c044d9251fa6f8d93ff9f6e4b22 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:38:24 -0500 Subject: [PATCH 37/40] Updated the description of SecretKeyName and SecretRef parameters in the CRD Signed-off-by: Theodor Mihalache --- .../api/v1alpha1/featurestore_types.go | 18 ++--- .../crd/bases/feast.dev_featurestores.yaml | 70 +++++++++---------- infra/feast-operator/dist/install.yaml | 70 +++++++++---------- 3 files changed, 77 insertions(+), 81 deletions(-) diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 9e15a28b671..17a029c02ea 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -108,10 +108,10 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;feast_trino.trino.TrinoOfflineStore;redis - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -151,10 +151,10 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the offline store service type OnlineStoreDBStorePersistence struct { // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } @@ -200,10 +200,10 @@ type RegistryFilePersistence struct { // RegistryDBStorePersistence configures the DB store persistence for the registry service type RegistryDBStorePersistence struct { // +kubebuilder:validation:Enum=sql;snowflake.registry - Type string `json:"type"` + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` - // By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - // SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + // By default, the selected store "type" is used as the SecretKeyName SecretKeyName string `json:"secretKeyName,omitempty"` } 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 ce4f36a29d4..1402a64056c 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -321,14 +321,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -679,14 +678,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1055,14 +1053,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1526,14 +1524,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1890,14 +1888,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2274,14 +2272,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index eedbec8a187..18ab82e9ca2 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -329,14 +329,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -687,14 +686,13 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. "registry_type" & "type" fields should be removed. properties: name: description: |- @@ -1063,14 +1061,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1534,14 +1532,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -1898,14 +1896,14 @@ spec: the DB store persistence for the offline store service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store "type" + is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. "registry_type" & "type" fields + should be removed. properties: name: description: |- @@ -2282,14 +2280,14 @@ spec: the DB store persistence for the registry service properties: secretKeyName: - description: |- - By default, the DB parameters should be placed as-is from the feature_store.yaml under the `type` key, if - SecretKeyName is specified the DB parameters should be placed as-is from the feature_store.yaml under the specified key + description: By default, the selected store + "type" is used as the SecretKeyName type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. "registry_type" & + "type" fields should be removed. properties: name: description: |- From 131e6e1940ab94d61c8f758dd64c947b87168a15 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Tue, 3 Dec 2024 13:46:23 -0500 Subject: [PATCH 38/40] Fixed error Signed-off-by: Theodor Mihalache --- .../controller/featurestore_controller_db_store_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 547edbc5181..60235fe687e 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -505,7 +505,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { svc) Expect(err).NotTo(HaveOccurred()) Expect(controllerutil.HasControllerReference(svc)).To(BeTrue()) - Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetPort)))) + Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) }) It("should properly encode a feature_store.yaml config", func() { From ddcac5c364552512edec53e9b4cf846448ec7a95 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Wed, 4 Dec 2024 09:52:07 -0500 Subject: [PATCH 39/40] Rebased to fix conflicts Signed-off-by: Theodor Mihalache --- .../featurestore_controller_test.go | 1 - .../controller/services/repo_config.go | 3 +- .../test/api/featurestore_types_test.go | 59 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index a6e71934fb0..44c81eca59a 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -964,7 +964,6 @@ var _ = Describe("FeatureStore Controller", func() { }, Spec: feastdevv1alpha1.FeatureStoreSpec{ FeastProject: referencedRegistry.Spec.FeastProject, - AuthzConfig: &feastdevv1alpha1.AuthzConfig{}, Services: &feastdevv1alpha1.FeatureStoreServices{ OnlineStore: &feastdevv1alpha1.OnlineStore{}, OfflineStore: &feastdevv1alpha1.OfflineStore{}, diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 0526220af5a..22052aa724d 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -49,7 +49,8 @@ func (feast *FeastServices) getServiceRepoConfig(feastType FeastServiceType) (Re func getServiceRepoConfig( feastType FeastServiceType, - featureStore *feastdevv1alpha1.FeatureStore, secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { + featureStore *feastdevv1alpha1.FeatureStore, + secretExtractionFunc func(secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { appliedSpec := featureStore.Status.Applied repoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc) diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index a5297111f23..302abef9384 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -290,6 +290,50 @@ func authzConfigWithOidc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv return fsCopy } +func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OnlineStore: &feastdevv1alpha1.OnlineStore{ + Persistence: &feastdevv1alpha1.OnlineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + OfflineStore: &feastdevv1alpha1.OfflineStore{ + Persistence: &feastdevv1alpha1.OfflineStorePersistence{ + DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + } + return fsCopy +} + +func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { + fsCopy := featureStore.DeepCopy() + fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{ + Local: &feastdevv1alpha1.LocalRegistryConfig{ + Persistence: &feastdevv1alpha1.RegistryPersistence{ + DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Type: dbPersistenceType, + }, + }, + }, + }, + } + return fsCopy +} + const resourceName = "test-resource" const namespaceName = "default" @@ -331,6 +375,10 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("s3://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") attemptInvalidCreationAndAsserts(ctx, onlineStoreWithObjectStoreBucketForPvc("gs://bucket/online_store.db", featurestore), "Online store does not support S3 or GS") }) + + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.online\", \"redis\", \"ikv\", \"datastore\", \"dynamodb\", \"bigtable\", \"postgres\", \"cassandra\", \"mysql\", \"hazelcast\", \"singlestore\"") + }) }) Context("When creating an invalid Offline Store", func() { @@ -339,6 +387,9 @@ var _ = Describe("FeatureStore API", func() { It("should fail when PVC persistence has absolute path", func() { attemptInvalidCreationAndAsserts(ctx, offlineStoreWithUnmanagedFileType(featurestore), "Unsupported value") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"snowflake.offline\", \"bigquery\", \"redshift\", \"spark\", \"postgres\", \"feast_trino.trino.TrinoOfflineStore\", \"redis\"") + }) }) Context("When creating an invalid Registry", func() { @@ -358,6 +409,9 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForFile(featurestore), "Additional S3 settings are available only for S3 object store URIs") attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) + It("should fail when db persistence type is invalid", func() { + attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: \"sql\", \"snowflake.registry\"") + }) }) Context("When creating an invalid PvcConfig", func() { @@ -395,11 +449,6 @@ 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).To(BeNil()) - }) }) Context("When omitting the AuthzConfig PvcConfig", func() { _, featurestore := initContext() From 1fbe9eacaaace1a05eae72bea62b6bd628f8f54f Mon Sep 17 00:00:00 2001 From: Theodor Mihalache Date: Wed, 4 Dec 2024 10:00:56 -0500 Subject: [PATCH 40/40] Rebased to fix conflicts Signed-off-by: Theodor Mihalache --- .../test/api/featurestore_types_test.go | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index f679e43c218..bffaf4052d2 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -272,26 +272,6 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } -func authzConfigWithKubernetes(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - fsCopy := featureStore.DeepCopy() - if fsCopy.Spec.AuthzConfig == nil { - fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} - } - fsCopy.Spec.AuthzConfig.KubernetesAuthz = &feastdevv1alpha1.KubernetesAuthz{ - Roles: []string{}, - } - return fsCopy -} -func authzConfigWithOidc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { - fsCopy := featureStore.DeepCopy() - if fsCopy.Spec.AuthzConfig == nil { - fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} - } - fsCopy.Spec.AuthzConfig.OidcAuthz = &feastdevv1alpha1.OidcAuthz{} - return fsCopy -} - - func authzConfigWithKubernetes(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { fsCopy := featureStore.DeepCopy() if fsCopy.Spec.AuthzConfig == nil {