@@ -56,16 +56,15 @@ type IpAddressClaimReconciler struct {
5656
5757// Reconcile is part of the main kubernetes reconciliation loop which aims to
5858// move the current state of the cluster closer to the desired state.
59- func (r * IpAddressClaimReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
59+ func (r * IpAddressClaimReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (result ctrl.Result , err error ) {
6060 logger := log .FromContext (ctx )
6161 debugLogger := logger .V (4 )
6262
6363 logger .Info ("reconcile loop started" )
6464
6565 /* 0. check if the matching IpAddressClaim object exists */
6666 o := & netboxv1.IpAddressClaim {}
67- err := r .Client .Get (ctx , req .NamespacedName , o )
68- if err != nil {
67+ if err := r .Client .Get (ctx , req .NamespacedName , o ); err != nil {
6968 return ctrl.Result {}, client .IgnoreNotFound (err )
7069 }
7170
@@ -77,18 +76,19 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
7776
7877 // Defer status update to ensure it happens regardless of how we exit
7978 // This follows Kubernetes controller best practices
79+ // The deferred function captures the return values to include error context in status
8080 defer func () {
81- if err := r .updateStatus (ctx , o , req .NamespacedName , debugLogger ); err != nil {
82- logger .Error (err , "failed to update IpAddressClaim status in deferred call" )
81+ if statusErr := r .updateStatus (ctx , o , req .NamespacedName , err , debugLogger ); statusErr != nil {
82+ logger .Error (statusErr , "failed to update IpAddressClaim status in deferred call" )
8383 }
84- }()
8584
86- // Set ready to false initially
87- if apismeta .FindStatusCondition (o .Status .Conditions , netboxv1 .ConditionReadyFalseNewResource .Type ) == nil {
88- if err := r .EventStatusRecorder .Report (ctx , o , netboxv1 .ConditionReadyFalseNewResource , corev1 .EventTypeNormal , nil ); err != nil {
89- return ctrl.Result {}, fmt .Errorf ("failed to initialise Ready condition: %w" , err )
85+ // If err is a StatusError, we've reported it in status conditions, so return nil to controller-runtime
86+ // This prevents exponential backoff for user-facing errors that are already visible in status
87+ // Regular errors are still returned to trigger retry with backoff
88+ if err != nil && IsStatusError (err ) {
89+ err = nil
9090 }
91- }
91+ }()
9292
9393 // 1. check if matching IpAddress object already exists
9494 ipAddress := & netboxv1.IpAddress {}
@@ -136,10 +136,7 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
136136 h := generateIpAddressRestorationHash (o )
137137 ipAddressModel , err := r .NetboxClient .RestoreExistingIpByHash (h )
138138 if err != nil {
139- if errReport := r .EventStatusRecorder .Report (ctx , o , netboxv1 .ConditionIpAssignedFalse , corev1 .EventTypeWarning , err ); errReport != nil {
140- return ctrl.Result {}, errReport
141- }
142- return ctrl.Result {Requeue : true }, nil
139+ return ctrl.Result {Requeue : true }, NewStatusError ("failed to restore existing IP by hash: %w" , err )
143140 }
144141
145142 if ipAddressModel == nil {
@@ -153,10 +150,7 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
153150 },
154151 })
155152 if err != nil {
156- if errReport := r .EventStatusRecorder .Report (ctx , o , netboxv1 .ConditionIpAssignedFalse , corev1 .EventTypeWarning , err ); errReport != nil {
157- return ctrl.Result {}, errReport
158- }
159- return ctrl.Result {Requeue : true }, nil
153+ return ctrl.Result {Requeue : true }, NewStatusError ("failed to get available IP address from NetBox: %w" , err )
160154 }
161155 debugLogger .Info (fmt .Sprintf ("ip address is not reserved in netbox, assigned new ip address: %s" , ipAddressModel .IpAddress ))
162156 } else {
@@ -172,15 +166,10 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
172166 }
173167
174168 if err := r .Client .Create (ctx , ipAddressResource ); err != nil {
175- if errReport := r .EventStatusRecorder .Report (ctx , o , netboxv1 .ConditionIpAssignedFalse , corev1 .EventTypeWarning , err ); errReport != nil {
176- return ctrl.Result {}, errReport
177- }
178- return ctrl.Result {}, fmt .Errorf ("failed to create IpAddress: %w" , err )
169+ return ctrl.Result {}, NewStatusError ("failed to create IpAddress: %w" , err )
179170 }
180171
181- if err := r .EventStatusRecorder .Report (ctx , o , netboxv1 .ConditionIpAssignedTrue , corev1 .EventTypeNormal , nil ); err != nil {
182- return ctrl.Result {}, fmt .Errorf ("failed to report IpAssigned condition: %w" , err )
183- }
172+ debugLogger .Info ("successfully created IpAddress resource" )
184173
185174 } else {
186175 // 6.b update fields of IPAddress object
@@ -220,20 +209,44 @@ func (r *IpAddressClaimReconciler) SetupWithManager(mgr ctrl.Manager) error {
220209
221210// updateStatus updates the IpAddressClaim status based on the current state of the owned IpAddress.
222211// This function is called as a deferred function in Reconcile to ensure status is always updated.
223- func (r * IpAddressClaimReconciler ) updateStatus (ctx context.Context , claim * netboxv1.IpAddressClaim , lookupKey types.NamespacedName , debugLogger logr.Logger ) error {
212+ // It captures any reconcile errors to include them in the status condition message.
213+ func (r * IpAddressClaimReconciler ) updateStatus (ctx context.Context , claim * netboxv1.IpAddressClaim , lookupKey types.NamespacedName , reconcileErr error , debugLogger logr.Logger ) error {
224214 debugLogger .Info ("updating ipaddressclaim status" )
225215
216+ // Initialize status conditions if this is a new resource
217+ if apismeta .FindStatusCondition (claim .Status .Conditions , netboxv1 .ConditionReadyFalseNewResource .Type ) == nil {
218+ if err := r .EventStatusRecorder .Report (ctx , claim , netboxv1 .ConditionReadyFalseNewResource , corev1 .EventTypeNormal , nil ); err != nil {
219+ return fmt .Errorf ("failed to initialise Ready condition: %w" , err )
220+ }
221+ }
222+
226223 // Fetch the latest IpAddress object
227224 ipAddress := & netboxv1.IpAddress {}
228225 if err := r .Client .Get (ctx , lookupKey , ipAddress ); err != nil {
229226 if apierrors .IsNotFound (err ) {
230- // IpAddress doesn't exist yet, this is expected during creation
231- debugLogger .Info ("ipaddress not found, skipping status update" )
227+ // IpAddress doesn't exist yet
228+ if reconcileErr != nil {
229+ // If we have an error and no IpAddress, it means IP assignment failed
230+ debugLogger .Info ("ipaddress not found with reconcile error, reporting IpAssigned false" )
231+ if err := r .EventStatusRecorder .Report (ctx , claim , netboxv1 .ConditionIpAssignedFalse , corev1 .EventTypeWarning , reconcileErr ); err != nil {
232+ return fmt .Errorf ("failed to report IpAssigned false condition: %w" , err )
233+ }
234+ } else {
235+ // No error and no IpAddress - this shouldn't happen in normal flow, skip update
236+ debugLogger .Info ("ipaddress not found without error, skipping status update" )
237+ }
232238 return nil
233239 }
234240 return fmt .Errorf ("failed to get IpAddress for status update: %w" , err )
235241 }
236242
243+ // IpAddress exists - report successful IP assignment if not already reported
244+ if apismeta .FindStatusCondition (claim .Status .Conditions , netboxv1 .ConditionIpAssignedTrue .Type ) == nil {
245+ if err := r .EventStatusRecorder .Report (ctx , claim , netboxv1 .ConditionIpAssignedTrue , corev1 .EventTypeNormal , nil ); err != nil {
246+ return fmt .Errorf ("failed to report IpAssigned condition: %w" , err )
247+ }
248+ }
249+
237250 // Update status based on IpAddress readiness
238251 if apismeta .IsStatusConditionTrue (ipAddress .Status .Conditions , "Ready" ) {
239252 debugLogger .Info ("ipaddress status ready true" )
@@ -245,7 +258,9 @@ func (r *IpAddressClaimReconciler) updateStatus(ctx context.Context, claim *netb
245258 }
246259 } else {
247260 debugLogger .Info ("ipaddress status ready false" )
248- if err := r .EventStatusRecorder .Report (ctx , claim , netboxv1 .ConditionIpClaimReadyFalse , corev1 .EventTypeWarning , nil ); err != nil {
261+ // Pass any reconcile error to the status condition
262+ // StatusErrors are user-facing, regular errors indicate transient/system issues
263+ if err := r .EventStatusRecorder .Report (ctx , claim , netboxv1 .ConditionIpClaimReadyFalse , corev1 .EventTypeWarning , reconcileErr ); err != nil {
249264 return fmt .Errorf ("failed to report IpClaimReady false condition: %w" , err )
250265 }
251266 }
0 commit comments