@@ -18,6 +18,7 @@ package controller
1818
1919import (
2020 "context"
21+ "encoding/json"
2122 "reflect"
2223 "time"
2324
@@ -36,6 +37,7 @@ import (
3637 "sigs.k8s.io/controller-runtime/pkg/log"
3738 "sigs.k8s.io/controller-runtime/pkg/reconcile"
3839
40+ feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1"
3941 feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1"
4042 "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz"
4143 feasthandler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler"
@@ -66,15 +68,49 @@ type FeatureStoreReconciler struct {
6668// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;create;update;watch;delete
6769// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
6870
71+ // convertV1ToV1Alpha1 converts a v1 FeatureStore to v1alpha1 for internal use
72+ // Since both types have identical structures, we use JSON marshaling/unmarshaling
73+ func convertV1ToV1Alpha1 (v1Obj * feastdevv1.FeatureStore ) * feastdevv1alpha1.FeatureStore {
74+ // Use JSON marshaling/unmarshaling since both types have identical JSON structure
75+ v1alpha1Obj := & feastdevv1alpha1.FeatureStore {
76+ ObjectMeta : v1Obj .ObjectMeta ,
77+ }
78+
79+ // Copy spec and status using JSON as intermediate format
80+ specData , err := json .Marshal (v1Obj .Spec )
81+ if err != nil {
82+ // If marshaling fails, return object with just metadata
83+ return v1alpha1Obj
84+ }
85+ if err := json .Unmarshal (specData , & v1alpha1Obj .Spec ); err != nil {
86+ // If unmarshaling fails, return object with just metadata
87+ return v1alpha1Obj
88+ }
89+ statusData , err := json .Marshal (v1Obj .Status )
90+ if err != nil {
91+ // If marshaling fails, return object with spec but no status
92+ return v1alpha1Obj
93+ }
94+ if err := json .Unmarshal (statusData , & v1alpha1Obj .Status ); err != nil {
95+ // If unmarshaling fails, return object with spec but no status
96+ return v1alpha1Obj
97+ }
98+
99+ return v1alpha1Obj
100+ }
101+
69102// Reconcile is part of the main kubernetes reconciliation loop which aims to
70103// move the current state of the cluster closer to the desired state.
71104// For more details, check Reconcile and its Result here:
72105// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile
73106func (r * FeatureStoreReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (result ctrl.Result , recErr error ) {
74107 logger := log .FromContext (ctx )
75108
76- cr := & feastdevv1alpha1.FeatureStore {}
77- err := r .Get (ctx , req .NamespacedName , cr )
109+ // Try to get as v1 first (storage version), then fall back to v1alpha1
110+ var cr * feastdevv1alpha1.FeatureStore
111+ var originalV1Obj * feastdevv1.FeatureStore
112+ v1Obj := & feastdevv1.FeatureStore {}
113+ err := r .Get (ctx , req .NamespacedName , v1Obj )
78114 if err != nil {
79115 if apierrors .IsNotFound (err ) {
80116 // CR deleted since request queued, child objects getting GC'd, no requeue
@@ -91,9 +127,33 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request
91127 }
92128 return ctrl.Result {}, nil
93129 }
94- // error fetching FeatureStore instance, requeue and try again
95- logger .Error (err , "Unable to get FeatureStore CR" )
96- return ctrl.Result {}, err
130+ // Try v1alpha1 if v1 fails
131+ v1alpha1Obj := & feastdevv1alpha1.FeatureStore {}
132+ err = r .Get (ctx , req .NamespacedName , v1alpha1Obj )
133+ if err != nil {
134+ if apierrors .IsNotFound (err ) {
135+ // CR deleted since request queued, child objects getting GC'd, no requeue
136+ logger .V (1 ).Info ("FeatureStore CR not found, has been deleted" )
137+ // Clean up namespace registry entry even if the CR is not found
138+ if err := r .cleanupNamespaceRegistry (ctx , & feastdevv1alpha1.FeatureStore {
139+ ObjectMeta : metav1.ObjectMeta {
140+ Name : req .NamespacedName .Name ,
141+ Namespace : req .NamespacedName .Namespace ,
142+ },
143+ }); err != nil {
144+ logger .Error (err , "Failed to clean up namespace registry entry for deleted FeatureStore" )
145+ // Don't return error here as the CR is already deleted
146+ }
147+ return ctrl.Result {}, nil
148+ }
149+ logger .Error (err , "Unable to get FeatureStore CR" )
150+ return ctrl.Result {}, err
151+ }
152+ cr = v1alpha1Obj
153+ } else {
154+ // Convert v1 to v1alpha1 for internal use
155+ originalV1Obj = v1Obj
156+ cr = convertV1ToV1Alpha1 (v1Obj )
97157 }
98158 currentStatus := cr .Status .DeepCopy ()
99159
@@ -109,7 +169,27 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request
109169
110170 result , recErr = r .deployFeast (ctx , cr )
111171 if cr .DeletionTimestamp == nil && ! reflect .DeepEqual (currentStatus , cr .Status ) {
112- if err = r .Client .Status ().Update (ctx , cr ); err != nil {
172+ // Update status - need to update in the original version (v1 if it was v1, v1alpha1 if it was v1alpha1)
173+ var statusObj client.Object
174+ if originalV1Obj != nil {
175+ // Convert back to v1 for status update
176+ originalV1Obj .Status = feastdevv1.FeatureStoreStatus {}
177+ statusData , err := json .Marshal (cr .Status )
178+ if err != nil {
179+ logger .Error (err , "Failed to marshal status for v1 conversion" )
180+ statusObj = cr
181+ } else {
182+ if err := json .Unmarshal (statusData , & originalV1Obj .Status ); err != nil {
183+ logger .Error (err , "Failed to unmarshal status for v1 conversion" )
184+ statusObj = cr
185+ } else {
186+ statusObj = originalV1Obj
187+ }
188+ }
189+ } else {
190+ statusObj = cr
191+ }
192+ if err = r .Client .Status ().Update (ctx , statusObj ); err != nil {
113193 if apierrors .IsConflict (err ) {
114194 logger .Info ("FeatureStore object modified, retry syncing status" )
115195 // Re-queue and preserve existing recErr
@@ -220,7 +300,7 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1
220300// SetupWithManager sets up the controller with the Manager.
221301func (r * FeatureStoreReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
222302 bldr := ctrl .NewControllerManagedBy (mgr ).
223- For (& feastdevv1alpha1 .FeatureStore {}).
303+ For (& feastdevv1 .FeatureStore {}).
224304 Owns (& corev1.ConfigMap {}).
225305 Owns (& appsv1.Deployment {}).
226306 Owns (& corev1.Service {}).
@@ -229,7 +309,12 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
229309 Owns (& rbacv1.RoleBinding {}).
230310 Owns (& rbacv1.Role {}).
231311 Owns (& batchv1.CronJob {}).
312+ Watches (& feastdevv1.FeatureStore {}, handler .EnqueueRequestsFromMapFunc (r .mapFeastRefsToFeastRequests )).
232313 Watches (& feastdevv1alpha1.FeatureStore {}, handler .EnqueueRequestsFromMapFunc (r .mapFeastRefsToFeastRequests ))
314+
315+ // Also watch v1alpha1 for backwards compatibility
316+ bldr = bldr .Watches (& feastdevv1alpha1.FeatureStore {}, & handler.EnqueueRequestForObject {})
317+
233318 if services .IsOpenShift () {
234319 bldr = bldr .Owns (& routev1.Route {})
235320 }
@@ -255,7 +340,18 @@ func (r *FeatureStoreReconciler) cleanupNamespaceRegistry(ctx context.Context, c
255340// if a remotely referenced FeatureStore is changed, reconcile any FeatureStores that reference it.
256341func (r * FeatureStoreReconciler ) mapFeastRefsToFeastRequests (ctx context.Context , object client.Object ) []reconcile.Request {
257342 logger := log .FromContext (ctx )
258- feastRef := object .(* feastdevv1alpha1.FeatureStore )
343+
344+ // Handle both v1 and v1alpha1 versions
345+ var feastRef * feastdevv1alpha1.FeatureStore
346+ switch obj := object .(type ) {
347+ case * feastdevv1.FeatureStore :
348+ feastRef = convertV1ToV1Alpha1 (obj )
349+ case * feastdevv1alpha1.FeatureStore :
350+ feastRef = obj
351+ default :
352+ logger .Error (nil , "Unexpected object type in mapFeastRefsToFeastRequests" )
353+ return nil
354+ }
259355
260356 // list all FeatureStores in the cluster
261357 var feastList feastdevv1alpha1.FeatureStoreList
0 commit comments