diff --git a/infra/feast-operator/go.mod b/infra/feast-operator/go.mod index c8608cb242f..3e41f468d68 100644 --- a/infra/feast-operator/go.mod +++ b/infra/feast-operator/go.mod @@ -21,6 +21,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect diff --git a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go index 883cfe940aa..fb12b207456 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go @@ -441,3 +441,132 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { }) }) }) + +var _ = Describe("Test mountCustomCABundle functionality", func() { + const resourceName = "test-cabundle" + const feastProject = "test_cabundle" + const configMapName = "odh-trusted-ca-bundle" + const caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" + const tlsPathCustomCABundle = "/etc/pki/tls/custom-certs/ca-bundle.crt" + + ctx := context.Background() + nsName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + fs := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: nsName.Namespace, + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1alpha1.FeatureStoreServices{ + Registry: &feastdevv1alpha1.Registry{Local: &feastdevv1alpha1.LocalRegistryConfig{Server: &feastdevv1alpha1.ServerConfigs{}}}, + OnlineStore: &feastdevv1alpha1.OnlineStore{Server: &feastdevv1alpha1.ServerConfigs{}}, + OfflineStore: &feastdevv1alpha1.OfflineStore{Server: &feastdevv1alpha1.ServerConfigs{}}, + UI: &feastdevv1alpha1.ServerConfigs{}, + }, + }, + } + + AfterEach(func() { + By("cleaning up FeatureStore and ConfigMap") + _ = k8sClient.Delete(ctx, &feastdevv1alpha1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: resourceName, Namespace: nsName.Namespace}}) + _ = k8sClient.Delete(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: nsName.Namespace}}) + }) + + It("should mount CA bundle volume and mounts in containers when ConfigMap exists", func() { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: nsName.Namespace, + Labels: map[string]string{ + caBundleAnnotation: "true", + }, + }, + } + Expect(k8sClient.Create(ctx, cm.DeepCopy())).To(Succeed()) + Expect(k8sClient.Create(ctx, fs.DeepCopy())).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + deploy := &appsv1.Deployment{} + objMeta := feast.GetObjectMeta() + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + + Expect(deploy.Spec.Template.Spec.Volumes).To(ContainElement(HaveField("Name", configMapName))) + for _, container := range deploy.Spec.Template.Spec.Containers { + Expect(container.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("Name", configMapName), + HaveField("MountPath", tlsPathCustomCABundle), + ))) + } + }) + + It("should not mount CA bundle volume or container mounts when ConfigMap is absent", func() { + Expect(k8sClient.Create(ctx, fs.DeepCopy())).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: nsName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, nsName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + deploy := &appsv1.Deployment{} + objMeta := feast.GetObjectMeta() + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + + Expect(deploy.Spec.Template.Spec.Volumes).NotTo(ContainElement(HaveField("Name", configMapName))) + for _, container := range deploy.Spec.Template.Spec.Containers { + Expect(container.VolumeMounts).NotTo(ContainElement(HaveField("Name", configMapName))) + } + }) +}) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index b81860ff464..c846725f3ba 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -34,12 +34,16 @@ const ( DefaultOnlineStorePath = "online_store.db" svcDomain = ".svc.cluster.local" - HttpPort = 80 - HttpsPort = 443 - HttpScheme = "http" - HttpsScheme = "https" - tlsPath = "/tls/" - tlsNameSuffix = "-tls" + HttpPort = 80 + HttpsPort = 443 + HttpScheme = "http" + HttpsScheme = "https" + tlsPath = "/tls/" + tlsPathCustomCABundle = "/etc/pki/tls/custom-certs/ca-bundle.crt" + tlsNameSuffix = "-tls" + + caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" + caBundleName = "odh-trusted-ca-bundle" DefaultOfflineStorageRequest = "20Gi" DefaultOnlineStorageRequest = "5Gi" @@ -268,3 +272,10 @@ type deploymentSettings struct { TargetHttpPort int32 TargetHttpsPort int32 } + +// CustomCertificatesBundle represents a custom CA bundle configuration +type CustomCertificatesBundle struct { + IsDefined bool + VolumeName string + ConfigMapName string +} diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go index 03a26a9031d..e3955f7d115 100644 --- a/infra/feast-operator/internal/controller/services/tls.go +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -19,6 +19,10 @@ package services import ( "strconv" + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/controller-runtime/pkg/log" + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" corev1 "k8s.io/api/core/v1" ) @@ -184,6 +188,7 @@ func (feast *FeastServices) mountTlsConfigs(podSpec *corev1.PodSpec) { feast.mountTlsConfig(OfflineFeastType, podSpec) feast.mountTlsConfig(OnlineFeastType, podSpec) feast.mountTlsConfig(UIFeastType, podSpec) + feast.mountCustomCABundle(podSpec) } func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec *corev1.PodSpec) { @@ -229,6 +234,63 @@ func mountTlsRemoteRegistryConfig(podSpec *corev1.PodSpec, tls *feastdevv1alpha1 } } +func (feast *FeastServices) mountCustomCABundle(podSpec *corev1.PodSpec) { + customCaBundle := feast.GetCustomCertificatesBundle() + if customCaBundle.IsDefined { + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: customCaBundle.VolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: customCaBundle.ConfigMapName}, + }, + }, + }) + + for i := range podSpec.Containers { + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ + Name: customCaBundle.VolumeName, + MountPath: tlsPathCustomCABundle, + ReadOnly: true, + SubPath: "ca-bundle.crt", + }) + } + + log.FromContext(feast.Handler.Context).Info("Mounted custom CA bundle ConfigMap to Feast pods.") + } +} + +// GetCustomCertificatesBundle retrieves the custom CA bundle ConfigMap if it exists when deployed with RHOAI or ODH +func (feast *FeastServices) GetCustomCertificatesBundle() CustomCertificatesBundle { + var customCertificatesBundle CustomCertificatesBundle + configMapList := &corev1.ConfigMapList{} + labelSelector := client.MatchingLabels{caBundleAnnotation: "true"} + + err := feast.Handler.Client.List( + feast.Handler.Context, + configMapList, + client.InNamespace(feast.Handler.FeatureStore.Namespace), + labelSelector, + ) + if err != nil { + log.FromContext(feast.Handler.Context).Error(err, "Error listing ConfigMaps. Not using custom CA bundle.") + return customCertificatesBundle + } + + // Check if caBundleName exists + for _, cm := range configMapList.Items { + if cm.Name == caBundleName { + log.FromContext(feast.Handler.Context).Info("Found trusted CA bundle ConfigMap. Using custom CA bundle.") + customCertificatesBundle.IsDefined = true + customCertificatesBundle.VolumeName = caBundleName + customCertificatesBundle.ConfigMapName = caBundleName + return customCertificatesBundle + } + } + + log.FromContext(feast.Handler.Context).Info("CA bundle ConfigMap named '" + caBundleName + "' not found. Not using custom CA bundle.") + return customCertificatesBundle +} + func getPortStr(tls *feastdevv1alpha1.TlsConfigs) string { if tls.IsTLS() { return strconv.Itoa(HttpsPort) diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index 04925ff02ee..caf694a2173 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -17,11 +17,12 @@ limitations under the License. package services import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "context" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -46,8 +47,10 @@ var _ = Describe("TLS Config", func() { // registry server w/o tls feast := FeastServices{ Handler: handler.FeastHandler{ - FeatureStore: minimalFeatureStore(), + Client: k8sClient, Scheme: scheme, + Context: context.TODO(), + FeatureStore: minimalFeatureStore(), }, } feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{