Skip to content

Commit 3ed5559

Browse files
committed
use statusError type to set condition in deffered funciton
1 parent 71c7422 commit 3ed5559

File tree

2 files changed

+72
-30
lines changed

2 files changed

+72
-30
lines changed

internal/controller/ipaddressclaim_controller.go

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

internal/controller/utils.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"strings"
2425

@@ -30,6 +31,32 @@ import (
3031
"sigs.k8s.io/controller-runtime/pkg/log"
3132
)
3233

34+
// StatusError wraps an error that should update status conditions.
35+
// Use NewStatusError to create errors that should be reflected in status.
36+
type StatusError struct {
37+
err error
38+
}
39+
40+
func (e *StatusError) Error() string {
41+
return e.err.Error()
42+
}
43+
44+
func (e *StatusError) Unwrap() error {
45+
return e.err
46+
}
47+
48+
// NewStatusError creates an error that will update the resource status condition.
49+
// Use this for errors that should be visible in kubectl describe output.
50+
func NewStatusError(format string, args ...interface{}) error {
51+
return &StatusError{err: fmt.Errorf(format, args...)}
52+
}
53+
54+
// IsStatusError checks if an error should update status conditions.
55+
func IsStatusError(err error) bool {
56+
var statusErr *StatusError
57+
return errors.As(err, &statusErr)
58+
}
59+
3360
func convertCIDRToLeaseLockName(cidr string) string {
3461
return strings.ReplaceAll(strings.ReplaceAll(cidr, "/", "-"), ":", "-")
3562
}

0 commit comments

Comments
 (0)