@@ -19,6 +19,7 @@ package controller
1919import (
2020 "context"
2121 "encoding/base64"
22+ "encoding/json"
2223 "fmt"
2324 "reflect"
2425 "strings"
@@ -1233,6 +1234,127 @@ var _ = Describe("FeatureStore Controller", func() {
12331234 Expect (cond .Message ).To (Equal ("Error: Remote feast registry of referenced FeatureStore '" + referencedRegistry .Name + "' is not ready" ))
12341235 })
12351236
1237+ It ("should allow cross-project registry references with different feastProject names" , func () {
1238+ By ("Reconciling the primary local registry FeatureStore" )
1239+ controllerReconciler := & FeatureStoreReconciler {
1240+ Client : k8sClient ,
1241+ Scheme : k8sClient .Scheme (),
1242+ }
1243+ _ , err := controllerReconciler .Reconcile (ctx , reconcile.Request {
1244+ NamespacedName : typeNamespacedName ,
1245+ })
1246+ Expect (err ).NotTo (HaveOccurred ())
1247+
1248+ primaryStore := & feastdevv1.FeatureStore {}
1249+ err = k8sClient .Get (ctx , typeNamespacedName , primaryStore )
1250+ Expect (err ).NotTo (HaveOccurred ())
1251+ Expect (primaryStore .Status .Applied .FeastProject ).To (Equal (feastProject ))
1252+
1253+ By ("Creating a second FeatureStore with a DIFFERENT feastProject name referencing the first" )
1254+ crossProjectName := "cross-project-ref"
1255+ crossProjectFeastName := "different_project"
1256+ crossProjectResource := & feastdevv1.FeatureStore {
1257+ ObjectMeta : metav1.ObjectMeta {
1258+ Name : crossProjectName ,
1259+ Namespace : primaryStore .Namespace ,
1260+ },
1261+ Spec : feastdevv1.FeatureStoreSpec {
1262+ FeastProject : crossProjectFeastName ,
1263+ Services : & feastdevv1.FeatureStoreServices {
1264+ OnlineStore : & feastdevv1.OnlineStore {
1265+ Server : & feastdevv1.ServerConfigs {},
1266+ },
1267+ Registry : & feastdevv1.Registry {
1268+ Remote : & feastdevv1.RemoteRegistryConfig {
1269+ FeastRef : & feastdevv1.FeatureStoreRef {
1270+ Name : primaryStore .Name ,
1271+ },
1272+ },
1273+ },
1274+ },
1275+ },
1276+ }
1277+ crossProjectResource .SetGroupVersionKind (feastdevv1 .GroupVersion .WithKind ("FeatureStore" ))
1278+ crossProjectNsName := client .ObjectKeyFromObject (crossProjectResource )
1279+ err = k8sClient .Create (ctx , crossProjectResource )
1280+ Expect (err ).NotTo (HaveOccurred ())
1281+
1282+ By ("Reconciling the cross-project FeatureStore — should succeed without error" )
1283+ _ , err = controllerReconciler .Reconcile (ctx , reconcile.Request {
1284+ NamespacedName : crossProjectNsName ,
1285+ })
1286+ Expect (err ).NotTo (HaveOccurred ())
1287+
1288+ err = k8sClient .Get (ctx , crossProjectNsName , crossProjectResource )
1289+ Expect (err ).NotTo (HaveOccurred ())
1290+
1291+ By ("Verifying the cross-project FeatureStore is ready and uses its own project name" )
1292+ Expect (crossProjectResource .Status .Applied .FeastProject ).To (Equal (crossProjectFeastName ))
1293+ Expect (crossProjectResource .Status .ServiceHostnames .Registry ).To (Equal (primaryStore .Status .ServiceHostnames .Registry ))
1294+ Expect (apimeta .IsStatusConditionTrue (crossProjectResource .Status .Conditions , feastdevv1 .OnlineStoreReadyType )).To (BeTrue ())
1295+
1296+ By ("Verifying the cross-project client ConfigMap uses the correct project name and shared registry" )
1297+ crossFeast := services.FeastServices {
1298+ Handler : handler.FeastHandler {
1299+ Client : controllerReconciler .Client ,
1300+ Context : ctx ,
1301+ Scheme : controllerReconciler .Scheme ,
1302+ FeatureStore : crossProjectResource ,
1303+ },
1304+ }
1305+ crossCm := & corev1.ConfigMap {}
1306+ err = k8sClient .Get (ctx , types.NamespacedName {
1307+ Name : crossFeast .GetFeastServiceName (services .ClientFeastType ),
1308+ Namespace : crossProjectResource .Namespace ,
1309+ }, crossCm )
1310+ Expect (err ).NotTo (HaveOccurred ())
1311+ crossRepoConfig := & services.RepoConfig {}
1312+ err = yaml .Unmarshal ([]byte (crossCm .Data [services .FeatureStoreYamlCmKey ]), crossRepoConfig )
1313+ Expect (err ).NotTo (HaveOccurred ())
1314+ Expect (crossRepoConfig .Project ).To (Equal (crossProjectFeastName ))
1315+ Expect (crossRepoConfig .Registry .Path ).To (ContainSubstring (primaryStore .Name ))
1316+
1317+ By ("Verifying the primary store client ConfigMap still uses its own project name" )
1318+ primaryFeast := services.FeastServices {
1319+ Handler : handler.FeastHandler {
1320+ Client : controllerReconciler .Client ,
1321+ Context : ctx ,
1322+ Scheme : controllerReconciler .Scheme ,
1323+ FeatureStore : primaryStore ,
1324+ },
1325+ }
1326+ primaryCm := & corev1.ConfigMap {}
1327+ err = k8sClient .Get (ctx , types.NamespacedName {
1328+ Name : primaryFeast .GetFeastServiceName (services .ClientFeastType ),
1329+ Namespace : primaryStore .Namespace ,
1330+ }, primaryCm )
1331+ Expect (err ).NotTo (HaveOccurred ())
1332+ primaryRepoConfig := & services.RepoConfig {}
1333+ err = yaml .Unmarshal ([]byte (primaryCm .Data [services .FeatureStoreYamlCmKey ]), primaryRepoConfig )
1334+ Expect (err ).NotTo (HaveOccurred ())
1335+ Expect (primaryRepoConfig .Project ).To (Equal (feastProject ))
1336+
1337+ By ("Verifying both stores share the same registry path" )
1338+ Expect (crossRepoConfig .Registry .Path ).To (Equal (primaryRepoConfig .Registry .Path ))
1339+
1340+ By ("Verifying the namespace registry ConfigMap lists both client configs" )
1341+ registryCm := & corev1.ConfigMap {}
1342+ err = k8sClient .Get (ctx , types.NamespacedName {
1343+ Name : services .NamespaceRegistryConfigMapName ,
1344+ Namespace : services .DefaultKubernetesNamespace ,
1345+ }, registryCm )
1346+ Expect (err ).NotTo (HaveOccurred ())
1347+ var registryData services.NamespaceRegistryData
1348+ err = json .Unmarshal ([]byte (registryCm .Data [services .NamespaceRegistryDataKey ]), & registryData )
1349+ Expect (err ).NotTo (HaveOccurred ())
1350+ ns := primaryStore .Namespace
1351+ Expect (registryData .Namespaces [ns ]).To (ContainElement (primaryFeast .GetFeastServiceName (services .ClientFeastType )))
1352+ Expect (registryData .Namespaces [ns ]).To (ContainElement (crossFeast .GetFeastServiceName (services .ClientFeastType )))
1353+
1354+ By ("Cleaning up the cross-project FeatureStore" )
1355+ Expect (k8sClient .Delete (ctx , crossProjectResource )).To (Succeed ())
1356+ })
1357+
12361358 It ("should correctly set container command args for grpc/rest modes" , func () {
12371359 controllerReconciler := & FeatureStoreReconciler {
12381360 Client : k8sClient ,
0 commit comments