@@ -112,14 +112,14 @@ func (r *AxonOpsAlertRouteReconciler) Reconcile(ctx context.Context, req ctrl.Re
112112 return ctrl.Result {}, nil
113113 }
114114
115- // Check idempotency: skip if already synced and generation unchanged
115+ // Check idempotency with drift detection
116116 readyCond := meta .FindStatusCondition (route .Status .Conditions , condTypeReady )
117117 if readyCond != nil && readyCond .Status == metav1 .ConditionTrue &&
118118 route .Status .ObservedGeneration == route .Generation &&
119119 route .Status .IntegrationID != "" {
120- log . Info ( "Route already synced, skipping reconciliation" ,
121- "integrationID" , route . Status . IntegrationID , "generation" , route . Generation )
122- return ctrl. Result {}, nil
120+ if result , done , err := r . checkDrift ( ctx , route ); done {
121+ return result , err
122+ }
123123 }
124124
125125 log .Info ("Reconciling AxonOpsAlertRoute" ,
@@ -199,10 +199,42 @@ func (r *AxonOpsAlertRouteReconciler) Reconcile(ctx context.Context, req ctrl.Re
199199 return ctrl.Result {}, err
200200 }
201201
202- // Check if route already exists
202+ // Set override and add route if needed
203203 routeExists := checkRouteExists (integrations , apiRouteType , route .Spec .Severity , integrationID )
204+ if result , stop := r .applyRoute (ctx , apiClient , route , apiRouteType , integrationID , routeExists ); stop {
205+ return result , nil
206+ }
207+
208+ // Update status
209+ now := metav1 .Now ()
210+ if err := r .Get (ctx , req .NamespacedName , route ); err != nil {
211+ return ctrl.Result {}, client .IgnoreNotFound (err )
212+ }
213+ route .Status .IntegrationID = integrationID
214+ route .Status .LastSyncTime = & now
215+ route .Status .ObservedGeneration = route .Generation
216+ meta .SetStatusCondition (& route .Status .Conditions , metav1.Condition {
217+ Type : condTypeReady ,
218+ Status : metav1 .ConditionTrue ,
219+ Reason : "RouteSynced" ,
220+ Message : "Alert route successfully synced with AxonOps" ,
221+ ObservedGeneration : route .Generation ,
222+ })
223+
224+ if err := r .Status ().Update (ctx , route ); err != nil {
225+ log .Error (err , "Failed to update status" )
226+ return ctrl.Result {}, err
227+ }
228+
229+ log .Info ("Successfully reconciled AxonOpsAlertRoute" )
230+ return ctrl.Result {}, nil
231+ }
232+
233+ // applyRoute sets the integration override (if needed) and adds the route (if missing).
234+ // Returns (result, true) if the reconcile loop should stop.
235+ func (r * AxonOpsAlertRouteReconciler ) applyRoute (ctx context.Context , apiClient * axonops.Client , route * alertsv1alpha1.AxonOpsAlertRoute , apiRouteType , integrationID string , routeExists bool ) (ctrl.Result , bool ) {
236+ log := logf .FromContext (ctx )
204237
205- // Set override if non-global and enabled (defaults to true when nil)
206238 enableOverride := route .Spec .EnableOverride == nil || * route .Spec .EnableOverride
207239 if route .Spec .Type != "global" && enableOverride {
208240 log .Info ("Setting integration override" , "routeType" , apiRouteType , "severity" , route .Spec .Severity )
@@ -216,58 +248,78 @@ func (r *AxonOpsAlertRouteReconciler) Reconcile(ctx context.Context, req ctrl.Re
216248 Message : common .SafeConditionMsg ("Failed to set override" , err ),
217249 ObservedGeneration : route .Generation ,
218250 })
219- if err := r .Status ().Update (ctx , route ); err != nil {
220- log .Error (err , "Failed to update status" )
251+ if statusErr := r .Status ().Update (ctx , route ); statusErr != nil {
252+ log .Error (statusErr , "Failed to update status" )
221253 }
222- return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
254+ return ctrl.Result {RequeueAfter : 30 * time .Second }, true
223255 }
224256 }
225257
226- // Add route if it doesn't exist
227- if ! routeExists {
228- log .Info ("Adding integration route" , "integrationID" , integrationID , "routeType" , apiRouteType , "severity" , route .Spec .Severity )
229- if err := apiClient .AddIntegrationRoute (ctx , route .Spec .ClusterType , route .Spec .ClusterName ,
230- apiRouteType , route .Spec .Severity , integrationID ); err != nil {
231- log .Error (err , "Failed to add integration route" )
232- meta .SetStatusCondition (& route .Status .Conditions , metav1.Condition {
233- Type : condTypeReady ,
234- Status : metav1 .ConditionFalse ,
235- Reason : "RouteError" ,
236- Message : common .SafeConditionMsg ("Failed to add route" , err ),
237- ObservedGeneration : route .Generation ,
238- })
239- if err := r .Status ().Update (ctx , route ); err != nil {
240- log .Error (err , "Failed to update status" )
241- }
242- return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
243- }
244- } else {
258+ if routeExists {
245259 log .Info ("Route already exists, skipping creation" )
260+ return ctrl.Result {}, false
246261 }
247262
248- // Update status
249- now := metav1 .Now ()
250- if err := r .Get (ctx , req .NamespacedName , route ); err != nil {
251- return ctrl.Result {}, client .IgnoreNotFound (err )
263+ log .Info ("Adding integration route" , "integrationID" , integrationID , "routeType" , apiRouteType , "severity" , route .Spec .Severity )
264+ if err := apiClient .AddIntegrationRoute (ctx , route .Spec .ClusterType , route .Spec .ClusterName ,
265+ apiRouteType , route .Spec .Severity , integrationID ); err != nil {
266+ log .Error (err , "Failed to add integration route" )
267+ meta .SetStatusCondition (& route .Status .Conditions , metav1.Condition {
268+ Type : condTypeReady ,
269+ Status : metav1 .ConditionFalse ,
270+ Reason : "RouteError" ,
271+ Message : common .SafeConditionMsg ("Failed to add route" , err ),
272+ ObservedGeneration : route .Generation ,
273+ })
274+ if statusErr := r .Status ().Update (ctx , route ); statusErr != nil {
275+ log .Error (statusErr , "Failed to update status" )
276+ }
277+ return ctrl.Result {RequeueAfter : 30 * time .Second }, true
252278 }
253- route .Status .IntegrationID = integrationID
254- route .Status .LastSyncTime = & now
255- route .Status .ObservedGeneration = route .Generation
279+ return ctrl.Result {}, false
280+ }
281+
282+ // checkDrift verifies whether the alert route still exists in AxonOps.
283+ // Returns (result, true, err) if the reconcile loop should return; (zero, false, nil) to continue.
284+ func (r * AxonOpsAlertRouteReconciler ) checkDrift (ctx context.Context , route * alertsv1alpha1.AxonOpsAlertRoute ) (ctrl.Result , bool , error ) {
285+ log := logf .FromContext (ctx )
286+ apiClient , err := ResolveAPIClient (ctx , r .Client , route .Namespace , route .Spec .ConnectionRef )
287+ if err != nil {
288+ log .Error (err , "Failed to resolve API client for drift check" )
289+ return ctrl.Result {RequeueAfter : common .DriftCheckInterval }, true , nil
290+ }
291+ integrations , err := apiClient .GetIntegrations (ctx , route .Spec .ClusterType , route .Spec .ClusterName )
292+ if err != nil {
293+ log .Error (err , "Failed to get integrations during drift check" )
294+ meta .SetStatusCondition (& route .Status .Conditions , metav1.Condition {
295+ Type : "DriftCheckFailed" ,
296+ Status : metav1 .ConditionTrue ,
297+ ObservedGeneration : route .Generation ,
298+ Reason : "APIError" ,
299+ Message : common .SafeConditionMsg ("Failed to check for drift" , err ),
300+ })
301+ if statusErr := r .Status ().Update (ctx , route ); statusErr != nil {
302+ log .Error (statusErr , "Failed to update status after drift check failure" )
303+ }
304+ return ctrl.Result {RequeueAfter : common .DriftCheckInterval }, true , nil
305+ }
306+ apiRouteType , _ := getAPIRouteType (route .Spec .Type )
307+ if checkRouteExists (integrations , apiRouteType , route .Spec .Severity , route .Status .IntegrationID ) {
308+ return ctrl.Result {RequeueAfter : common .DriftCheckInterval }, true , nil
309+ }
310+ log .Info ("Drift detected: alert route no longer exists in AxonOps, will recreate" )
256311 meta .SetStatusCondition (& route .Status .Conditions , metav1.Condition {
257- Type : condTypeReady ,
312+ Type : "DriftDetected" ,
258313 Status : metav1 .ConditionTrue ,
259- Reason : "RouteSynced" ,
260- Message : "Alert route successfully synced with AxonOps" ,
261314 ObservedGeneration : route .Generation ,
315+ Reason : "ResourceDeleted" ,
316+ Message : "Alert route was deleted externally; reapplying" ,
262317 })
263-
264- if err := r .Status ().Update (ctx , route ); err != nil {
265- log .Error (err , "Failed to update status" )
266- return ctrl.Result {}, err
318+ route .Status .IntegrationID = ""
319+ if statusErr := r .Status ().Update (ctx , route ); statusErr != nil {
320+ return ctrl.Result {}, true , statusErr
267321 }
268-
269- log .Info ("Successfully reconciled AxonOpsAlertRoute" )
270- return ctrl.Result {}, nil
322+ return ctrl.Result {}, false , nil
271323}
272324
273325// handleDeletion handles cleanup when the CR is being deleted
0 commit comments