@@ -2,8 +2,11 @@ package controllers
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
5
6
"time"
6
7
8
+ appsv1 "k8s.io/api/apps/v1"
9
+ corev1 "k8s.io/api/core/v1"
7
10
"k8s.io/apimachinery/pkg/api/errors"
8
11
"k8s.io/apimachinery/pkg/runtime"
9
12
ctrl "sigs.k8s.io/controller-runtime"
@@ -12,6 +15,8 @@ import (
12
15
"sigs.k8s.io/controller-runtime/pkg/log"
13
16
14
17
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
18
+ "github.com/stacklok/toolhive/cmd/thv-operator/pkg/mcpregistrystatus"
19
+ "github.com/stacklok/toolhive/cmd/thv-operator/pkg/registryapi"
15
20
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/sources"
16
21
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/sync"
17
22
)
@@ -22,19 +27,27 @@ type MCPRegistryReconciler struct {
22
27
Scheme * runtime.Scheme
23
28
24
29
// Sync manager handles all sync operations
25
- syncManager sync.Manager
30
+ syncManager sync.Manager
31
+ storageManager sources.StorageManager
32
+ sourceHandlerFactory sources.SourceHandlerFactory
33
+ // Registry API manager handles API deployment operations
34
+ registryAPIManager registryapi.Manager
26
35
}
27
36
28
37
// NewMCPRegistryReconciler creates a new MCPRegistryReconciler with required dependencies
29
38
func NewMCPRegistryReconciler (k8sClient client.Client , scheme * runtime.Scheme ) * MCPRegistryReconciler {
30
39
sourceHandlerFactory := sources .NewSourceHandlerFactory (k8sClient )
31
40
storageManager := sources .NewConfigMapStorageManager (k8sClient , scheme )
32
41
syncManager := sync .NewDefaultSyncManager (k8sClient , scheme , sourceHandlerFactory , storageManager )
42
+ registryAPIManager := registryapi .NewManager (k8sClient , scheme , storageManager , sourceHandlerFactory )
33
43
34
44
return & MCPRegistryReconciler {
35
- Client : k8sClient ,
36
- Scheme : scheme ,
37
- syncManager : syncManager ,
45
+ Client : k8sClient ,
46
+ Scheme : scheme ,
47
+ syncManager : syncManager ,
48
+ storageManager : storageManager ,
49
+ sourceHandlerFactory : sourceHandlerFactory ,
50
+ registryAPIManager : registryAPIManager ,
38
51
}
39
52
}
40
53
@@ -43,9 +56,15 @@ func NewMCPRegistryReconciler(k8sClient client.Client, scheme *runtime.Scheme) *
43
56
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpregistries/finalizers,verbs=update
44
57
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
45
58
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
59
+ //
60
+ // For creating registry-api deployment and service
61
+ // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
62
+ // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
46
63
47
64
// Reconcile is part of the main kubernetes reconciliation loop which aims to
48
65
// move the current state of the cluster closer to the desired state.
66
+ //
67
+ //nolint:gocyclo // Complex reconciliation logic requires multiple conditions
49
68
func (r * MCPRegistryReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
50
69
ctxLogger := log .FromContext (ctx )
51
70
@@ -106,8 +125,42 @@ func (r *MCPRegistryReconciler) Reconcile(ctx context.Context, req ctrl.Request)
106
125
return ctrl.Result {}, nil
107
126
}
108
127
109
- // 3. Check if sync is needed before performing it
110
- result , err := r .reconcileSync (ctx , mcpRegistry )
128
+ // 3. Create status collector for batched updates
129
+ statusCollector := mcpregistrystatus .NewCollector (mcpRegistry )
130
+
131
+ // 4. Reconcile sync operation
132
+ result , syncErr := r .reconcileSync (ctx , mcpRegistry , statusCollector )
133
+
134
+ // 5. Reconcile API service (deployment and service, independent of sync status)
135
+ if syncErr == nil {
136
+ if apiErr := r .registryAPIManager .ReconcileAPIService (ctx , mcpRegistry , statusCollector ); apiErr != nil {
137
+ ctxLogger .Error (apiErr , "Failed to reconcile API service" )
138
+ if syncErr == nil {
139
+ err = apiErr
140
+ }
141
+ }
142
+ }
143
+
144
+ // 6. Check if we need to requeue for API readiness
145
+ if syncErr == nil && ! r .registryAPIManager .IsAPIReady (ctx , mcpRegistry ) {
146
+ ctxLogger .Info ("API not ready yet, scheduling requeue to check readiness" )
147
+ if result .RequeueAfter == 0 || result .RequeueAfter > time .Second * 30 {
148
+ result .RequeueAfter = time .Second * 30
149
+ }
150
+ }
151
+
152
+ // 7. Apply all status changes in a single batch update
153
+ if statusUpdateErr := statusCollector .Apply (ctx , r .Status ()); statusUpdateErr != nil {
154
+ ctxLogger .Error (statusUpdateErr , "Failed to apply batched status update" )
155
+ // Return the status update error only if there was no main reconciliation error
156
+ if syncErr == nil {
157
+ err = statusUpdateErr
158
+ }
159
+ }
160
+
161
+ if err == nil {
162
+ err = syncErr
163
+ }
111
164
112
165
// Log reconciliation completion
113
166
if err != nil {
@@ -124,16 +177,13 @@ func (r *MCPRegistryReconciler) Reconcile(ctx context.Context, req ctrl.Request)
124
177
}
125
178
126
179
// reconcileSync checks if sync is needed and performs it if necessary
127
- func (r * MCPRegistryReconciler ) reconcileSync (ctx context.Context , mcpRegistry * mcpv1alpha1.MCPRegistry ) (ctrl.Result , error ) {
180
+ // This method only handles data synchronization to the target ConfigMap
181
+ func (r * MCPRegistryReconciler ) reconcileSync (
182
+ ctx context.Context , mcpRegistry * mcpv1alpha1.MCPRegistry , statusCollector mcpregistrystatus.Collector ,
183
+ ) (ctrl.Result , error ) {
128
184
ctxLogger := log .FromContext (ctx )
129
185
130
- // Refresh the object to get latest status for accurate timing calculations
131
- if err := r .Get (ctx , client .ObjectKeyFromObject (mcpRegistry ), mcpRegistry ); err != nil {
132
- ctxLogger .Error (err , "Failed to refresh MCPRegistry object for sync check" )
133
- return ctrl.Result {}, err
134
- }
135
-
136
- // Check if sync is needed
186
+ // Check if sync is needed - no need to refresh object here since we just fetched it
137
187
syncNeeded , syncReason , nextSyncTime , err := r .syncManager .ShouldSync (ctx , mcpRegistry )
138
188
if err != nil {
139
189
ctxLogger .Error (err , "Failed to determine if sync is needed" )
@@ -155,6 +205,10 @@ func (r *MCPRegistryReconciler) reconcileSync(ctx context.Context, mcpRegistry *
155
205
156
206
ctxLogger .Info ("Sync needed" , "reason" , syncReason )
157
207
208
+ // Set phase to syncing before starting the sync process
209
+ statusCollector .SetPhase (mcpv1alpha1 .MCPRegistryPhaseSyncing )
210
+ statusCollector .SetMessage ("Syncing registry data" )
211
+
158
212
// Handle manual sync with no data changes - update trigger tracking only
159
213
if syncReason == sync .ReasonManualNoChanges {
160
214
return r .syncManager .UpdateManualSyncTriggerOnly (ctx , mcpRegistry )
@@ -165,6 +219,8 @@ func (r *MCPRegistryReconciler) reconcileSync(ctx context.Context, mcpRegistry *
165
219
if err != nil {
166
220
// Sync failed - schedule retry with exponential backoff
167
221
ctxLogger .Error (err , "Sync failed, scheduling retry" )
222
+ statusCollector .SetPhase (mcpv1alpha1 .MCPRegistryPhaseFailed )
223
+ statusCollector .SetMessage (fmt .Sprintf ("Sync failed: %v" , err ))
168
224
// Use a shorter retry interval instead of the full sync interval
169
225
retryAfter := time .Minute * 5 // Default retry interval
170
226
if result .RequeueAfter > 0 {
@@ -174,6 +230,11 @@ func (r *MCPRegistryReconciler) reconcileSync(ctx context.Context, mcpRegistry *
174
230
return ctrl.Result {RequeueAfter : retryAfter }, err
175
231
}
176
232
233
+ // Sync successful - keep in syncing phase until API is also ready
234
+ statusCollector .SetMessage ("Registry data synced successfully" )
235
+
236
+ ctxLogger .Info ("Registry data sync completed successfully" )
237
+
177
238
// Schedule next automatic sync only if this was an automatic sync (not manual)
178
239
if mcpRegistry .Spec .SyncPolicy != nil && ! sync .IsManualSync (syncReason ) {
179
240
interval , parseErr := time .ParseDuration (mcpRegistry .Spec .SyncPolicy .Interval )
@@ -196,7 +257,7 @@ func (r *MCPRegistryReconciler) reconcileSync(ctx context.Context, mcpRegistry *
196
257
func (r * MCPRegistryReconciler ) finalizeMCPRegistry (ctx context.Context , registry * mcpv1alpha1.MCPRegistry ) error {
197
258
ctxLogger := log .FromContext (ctx )
198
259
199
- // Update the MCPRegistry status to indicate termination
260
+ // Update the MCPRegistry status to indicate termination - immediate update needed since object is being deleted
200
261
registry .Status .Phase = mcpv1alpha1 .MCPRegistryPhaseTerminating
201
262
registry .Status .Message = "MCPRegistry is being terminated"
202
263
if err := r .Status ().Update (ctx , registry ); err != nil {
@@ -211,7 +272,7 @@ func (r *MCPRegistryReconciler) finalizeMCPRegistry(ctx context.Context, registr
211
272
}
212
273
213
274
// TODO: Add additional cleanup logic when other features are implemented:
214
- // - Clean up Registry API service
275
+ // - Clean up Registry API deployment and service (will be handled by owner references)
215
276
// - Cancel any running sync operations
216
277
217
278
ctxLogger .Info ("MCPRegistry finalization completed" , "registry" , registry .Name )
@@ -222,5 +283,8 @@ func (r *MCPRegistryReconciler) finalizeMCPRegistry(ctx context.Context, registr
222
283
func (r * MCPRegistryReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
223
284
return ctrl .NewControllerManagedBy (mgr ).
224
285
For (& mcpv1alpha1.MCPRegistry {}).
286
+ Owns (& appsv1.Deployment {}).
287
+ Owns (& corev1.Service {}).
288
+ Owns (& corev1.ConfigMap {}).
225
289
Complete (r )
226
290
}
0 commit comments