Skip to content

Commit 7204b59

Browse files
committed
refactor controller
1 parent 128e6cb commit 7204b59

File tree

9 files changed

+323
-180
lines changed

9 files changed

+323
-180
lines changed

api/v1/ipaddressclaim_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,10 @@ var ConditionIpAssignedFalse = metav1.Condition{
117117
Reason: "IPAddressCRNotCreated",
118118
Message: "Failed to fetch new IP from NetBox",
119119
}
120+
121+
var ConditionIpAssignedFalseSizeMissmatch = metav1.Condition{
122+
Type: "IPAssigned",
123+
Status: "False",
124+
Reason: "IPAddressCRNotCreated",
125+
Message: "Size of restored IpRange does not match the requested size",
126+
}

api/v1/iprange_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ type IpRangeStatus struct {
6565
//+kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`
6666
//+kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.status.url`
6767
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
68-
// +kubebuilder:resource:shortName=ipr
68+
// +kubebuilder:resource:shortName=ir
6969

7070
// IpRange is the Schema for the ipranges API
7171
type IpRange struct {

api/v1/iprangeclaim_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type IpRangeClaimStatus struct {
6363
//+kubebuilder:printcolumn:name="IpRangeAssigned",type=string,JSONPath=`.status.conditions[?(@.type=="IPAssigned")].status`
6464
//+kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
6565
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
66-
// +kubebuilder:resource:shortName=iprc
66+
// +kubebuilder:resource:shortName=irc
6767

6868
// IpRangeClaim is the Schema for the iprangeclaims API
6969
type IpRangeClaim struct {

internal/controller/iprange_controller.go

Lines changed: 161 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ package controller
1919
import (
2020
"context"
2121
"encoding/json"
22-
"errors"
2322
"fmt"
2423
"maps"
2524
"strconv"
2625
"strings"
27-
"time"
2826

2927
netboxv1 "github.com/netbox-community/netbox-operator/api/v1"
3028
"github.com/netbox-community/netbox-operator/pkg/config"
@@ -77,33 +75,13 @@ func (r *IpRangeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
7775

7876
// if being deleted
7977
if !o.ObjectMeta.DeletionTimestamp.IsZero() {
80-
if controllerutil.ContainsFinalizer(o, IpRangeFinalizerName) {
81-
if !o.Spec.PreserveInNetbox {
82-
err := r.NetboxClient.DeleteIpRange(o.Status.IpRangeId)
83-
if err != nil {
84-
setConditionErr := r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyFalseDeletionFailed, corev1.EventTypeWarning, err.Error())
85-
if setConditionErr != nil {
86-
return ctrl.Result{}, fmt.Errorf("error updating status: %w, when deletion of IpRange failed: %w", setConditionErr, err)
87-
}
88-
89-
return ctrl.Result{Requeue: true}, nil
90-
}
91-
}
92-
93-
debugLogger.Info("removing the finalizer")
94-
removed := controllerutil.RemoveFinalizer(o, IpRangeFinalizerName)
95-
if !removed {
96-
return ctrl.Result{}, errors.New("failed to remove the finalizer")
97-
}
98-
99-
err = r.Update(ctx, o)
100-
if err != nil {
101-
return ctrl.Result{}, err
102-
}
78+
if o.Spec.PreserveInNetbox {
79+
// if there's a finalizer remove it and return
80+
// this can be the case if a CR used to have spec.preserveInNetbox set to false
81+
return r.removeFinalizer(ctx, o)
10382
}
10483

105-
// end loop if deletion timestamp is not zero
106-
return ctrl.Result{}, nil
84+
return r.deleteFromNetboxAndRemoveFinalizer(ctx, o)
10785
}
10886

10987
// if PreserveIpInNetbox flag is false then register finalizer if not yet registered
@@ -118,43 +96,16 @@ func (r *IpRangeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
11896
// 1. try to lock lease of parent prefix if IpRange status condition is not true
11997
// and IpRange is owned by an IpRangeClaim
12098
or := o.ObjectMeta.OwnerReferences
121-
var ll *leaselocker.LeaseLocker
12299
if len(or) > 0 /* len(nil array) = 0 */ && !apismeta.IsStatusConditionTrue(o.Status.Conditions, "Ready") {
123-
// get ip range claim
124-
orLookupKey := types.NamespacedName{
125-
Name: or[0].Name,
126-
Namespace: req.Namespace,
127-
}
128-
ipRangeClaim := &netboxv1.IpRangeClaim{}
129-
err = r.Client.Get(ctx, orLookupKey, ipRangeClaim)
130-
if err != nil {
131-
return ctrl.Result{}, err
132-
}
133-
134-
// get name of parent prefix
135-
leaseLockerNSN := types.NamespacedName{
136-
Name: convertCIDRToLeaseLockName(ipRangeClaim.Spec.ParentPrefix),
137-
Namespace: r.OperatorNamespace,
138-
}
139-
ll, err = leaselocker.NewLeaseLocker(r.RestConfig, leaseLockerNSN, req.NamespacedName.String())
100+
ll, res, err := r.tryLockOnParentPrefix(ctx, o)
101+
defer func() {
102+
if ll != nil {
103+
ll.Unlock()
104+
}
105+
}()
140106
if err != nil {
141-
return ctrl.Result{}, err
142-
}
143-
144-
lockCtx, cancel := context.WithCancel(ctx)
145-
defer cancel()
146-
147-
// create lock
148-
locked := ll.TryLock(lockCtx)
149-
if !locked {
150-
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", ipRangeClaim.Spec.ParentPrefix))
151-
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
152-
ipRangeClaim.Spec.ParentPrefix)
153-
return ctrl.Result{
154-
RequeueAfter: 2 * time.Second,
155-
}, nil
107+
return res, err
156108
}
157-
debugLogger.Info(fmt.Sprintf("successfully locked parent prefix %s", ipRangeClaim.Spec.ParentPrefix))
158109
}
159110

160111
// 2. reserve or update ip range in netbox
@@ -164,67 +115,34 @@ func (r *IpRangeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
164115
return ctrl.Result{}, err
165116
}
166117

167-
ipRangeModel, err := generateNetboxIpRangeModelFromIpRangeSpec(&o.Spec, req, annotations[LastIpRangeMetadataAnnotationName])
118+
ipRangeModel, err := r.generateNetboxIpRangeModelFromIpRangeSpec(ctx, o, req, annotations[LastIpRangeMetadataAnnotationName])
168119
if err != nil {
169120
return ctrl.Result{}, err
170121
}
171122

172123
netboxIpRangeModel, err := r.NetboxClient.ReserveOrUpdateIpRange(ipRangeModel)
173124
if err != nil {
174-
updateStatusErr := r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyFalse,
175-
corev1.EventTypeWarning, fmt.Sprintf("%s-%s", o.Spec.StartAddress, o.Spec.EndAddress))
176-
return ctrl.Result{}, fmt.Errorf("failed to update ip range status: %w, "+
177-
"after reservation of ip in netbox failed: %w", updateStatusErr, err)
178-
}
179-
180-
// 3. unlock lease of parent prefix
181-
if ll != nil {
182-
ll.Unlock()
125+
err := r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyFalse,
126+
corev1.EventTypeWarning, fmt.Sprintf("%s-%s", o.Spec.StartAddress, o.Spec.EndAddress), err)
127+
if err != nil {
128+
return ctrl.Result{}, nil
129+
}
183130
}
184131

185132
// 4. update status fields
186133
o.Status.IpRangeId = netboxIpRangeModel.ID
187-
o.Status.IpRangeUrl = config.GetBaseUrl() + "/ipam/ip-rages/" + strconv.FormatInt(netboxIpRangeModel.ID, 10)
134+
o.Status.IpRangeUrl = config.GetBaseUrl() + "/ipam/ip-ranges/" + strconv.FormatInt(netboxIpRangeModel.ID, 10)
188135
err = r.Client.Status().Update(ctx, o)
189136
if err != nil {
190137
return ctrl.Result{}, err
191138
}
192139

193-
// update lastIpRangeMetadata annotation
194-
if annotations == nil {
195-
annotations = make(map[string]string)
196-
}
197-
198-
if len(o.Spec.CustomFields) > 0 {
199-
lastIpRangeMetadata, err := json.Marshal(o.Spec.CustomFields)
200-
if err != nil {
201-
return ctrl.Result{}, fmt.Errorf("failed to marshal lastIpRangeMetadata annotation: %w", err)
202-
}
203-
204-
annotations[LastIpRangeMetadataAnnotationName] = string(lastIpRangeMetadata)
205-
} else {
206-
annotations[LastIpRangeMetadataAnnotationName] = "{}"
207-
}
208-
209-
err = accessor.SetAnnotations(o, annotations)
140+
err = r.updateLastMetadataAnnotation(ctx, annotations, o, accessor)
210141
if err != nil {
211142
return ctrl.Result{}, err
212143
}
213144

214-
// update object to store lastIpRangeMetadata annotation
215-
if err := r.Update(ctx, o); err != nil {
216-
return ctrl.Result{}, err
217-
}
218-
219-
// check if created ip range contains entire description from spec
220-
_, found := strings.CutPrefix(netboxIpRangeModel.Description, req.NamespacedName.String()+" // "+o.Spec.Description)
221-
if !found {
222-
r.Recorder.Event(o, corev1.EventTypeWarning, "IpRangeDescriptionTruncated", "ip range was created with truncated description")
223-
}
224-
225-
debugLogger.Info(fmt.Sprintf("reserved ip range in netbox, start address: %s, end address: %s", o.Spec.StartAddress, o.Spec.EndAddress))
226-
227-
err = r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyTrue, corev1.EventTypeNormal, "")
145+
err = r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyTrue, corev1.EventTypeNormal, "", nil)
228146
if err != nil {
229147
return ctrl.Result{}, err
230148
}
@@ -241,22 +159,33 @@ func (r *IpRangeReconciler) SetupWithManager(mgr ctrl.Manager) error {
241159
Complete(r)
242160
}
243161

244-
func (r *IpRangeReconciler) SetConditionAndCreateEvent(ctx context.Context, o *netboxv1.IpRange, condition metav1.Condition, eventType string, conditionMessageAppend string) error {
162+
func (r *IpRangeReconciler) SetConditionAndCreateEvent(ctx context.Context, o *netboxv1.IpRange, condition metav1.Condition, eventType string, conditionMessageAppend string, errIn error) error {
245163
if len(conditionMessageAppend) > 0 {
246164
condition.Message = condition.Message + ". " + conditionMessageAppend
247165
}
166+
167+
if errIn != nil {
168+
// log errors here, so we do not need to aggregate them in the reconcile loop with the error of updating the status
169+
logger := log.FromContext(ctx)
170+
logger.Error(errIn, condition.Message+errIn.Error())
171+
condition.Message = condition.Message + ". Check the logs for more information."
172+
}
173+
248174
conditionChanged := apismeta.SetStatusCondition(&o.Status.Conditions, condition)
249175
if conditionChanged {
250176
r.Recorder.Event(o, eventType, condition.Reason, condition.Message)
251177
}
178+
252179
err := r.Client.Status().Update(ctx, o)
253180
if err != nil {
254181
return err
255182
}
183+
256184
return nil
257185
}
258186

259-
func generateNetboxIpRangeModelFromIpRangeSpec(spec *netboxv1.IpRangeSpec, req ctrl.Request, lastIpRangeMetadata string) (*models.IpRange, error) {
187+
func (r *IpRangeReconciler) generateNetboxIpRangeModelFromIpRangeSpec(ctx context.Context, o *netboxv1.IpRange, req ctrl.Request, lastIpRangeMetadata string) (*models.IpRange, error) {
188+
debugLogger := log.FromContext(context.Background()).V(4)
260189
// unmarshal lastIpRangeMetadata json string to map[string]string
261190
lastAppliedCustomFields := make(map[string]string)
262191
if lastIpRangeMetadata != "" {
@@ -266,8 +195,8 @@ func generateNetboxIpRangeModelFromIpRangeSpec(spec *netboxv1.IpRangeSpec, req c
266195
}
267196

268197
netboxCustomFields := make(map[string]string)
269-
if len(spec.CustomFields) > 0 {
270-
netboxCustomFields = maps.Clone(spec.CustomFields)
198+
if len(o.Spec.CustomFields) > 0 {
199+
netboxCustomFields = maps.Clone(o.Spec.CustomFields)
271200
}
272201

273202
// if a custom field was removed from the spec, add it with an empty value
@@ -279,14 +208,134 @@ func generateNetboxIpRangeModelFromIpRangeSpec(spec *netboxv1.IpRangeSpec, req c
279208
}
280209
}
281210

211+
description := api.TruncateDescription(req.NamespacedName.String() + " // " + o.Spec.Description)
212+
213+
// check if created ip range contains entire description from spec
214+
_, found := strings.CutPrefix(description, req.NamespacedName.String()+" // "+o.Spec.Description)
215+
if !found {
216+
r.Recorder.Event(o, corev1.EventTypeWarning, "IpRangeDescriptionTruncated", "ip range was created with truncated description")
217+
}
218+
219+
debugLogger.Info(fmt.Sprintf("reserved ip range in netbox, start address: %s, end address: %s", o.Spec.StartAddress, o.Spec.EndAddress))
220+
282221
return &models.IpRange{
283-
StartAddress: spec.StartAddress,
284-
EndAddress: spec.EndAddress,
222+
StartAddress: o.Spec.StartAddress,
223+
EndAddress: o.Spec.EndAddress,
285224
Metadata: &models.NetboxMetadata{
286-
Comments: spec.Comments,
225+
Comments: o.Spec.Comments,
287226
Custom: netboxCustomFields,
288-
Description: req.NamespacedName.String() + " // " + spec.Description,
289-
Tenant: spec.Tenant,
227+
Description: description,
228+
Tenant: o.Spec.Tenant,
290229
},
291230
}, nil
292231
}
232+
233+
func (r *IpRangeReconciler) deleteFromNetboxAndRemoveFinalizer(ctx context.Context, o *netboxv1.IpRange) (ctrl.Result, error) {
234+
err := r.NetboxClient.DeleteIpRange(o.Status.IpRangeId)
235+
if err != nil {
236+
err = r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpRangeReadyFalseDeletionFailed,
237+
corev1.EventTypeWarning, "", err)
238+
if err != nil {
239+
return ctrl.Result{}, err
240+
}
241+
242+
return ctrl.Result{Requeue: true}, nil
243+
}
244+
245+
res, err := r.removeFinalizer(ctx, o)
246+
if err != nil {
247+
return res, err
248+
}
249+
250+
err = r.Update(ctx, o)
251+
if err != nil {
252+
return ctrl.Result{}, err
253+
}
254+
255+
return ctrl.Result{}, nil
256+
}
257+
258+
func (r *IpRangeReconciler) tryLockOnParentPrefix(ctx context.Context, o *netboxv1.IpRange) (*leaselocker.LeaseLocker, ctrl.Result, error) {
259+
// determine NamespacedName of IpRangeClaim owning the IpRange CR
260+
orLookupKey := types.NamespacedName{
261+
Name: o.ObjectMeta.OwnerReferences[0].Name,
262+
Namespace: o.Namespace,
263+
}
264+
265+
logger := log.FromContext(ctx)
266+
debugLogger := log.FromContext(ctx).V(4)
267+
268+
ipRangeClaim := &netboxv1.IpRangeClaim{}
269+
err := r.Client.Get(ctx, orLookupKey, ipRangeClaim)
270+
if err != nil {
271+
return nil, ctrl.Result{}, err
272+
}
273+
274+
// get name of parent prefix
275+
leaseLockerNSN := types.NamespacedName{
276+
Name: convertCIDRToLeaseLockName(ipRangeClaim.Spec.ParentPrefix),
277+
Namespace: r.OperatorNamespace,
278+
}
279+
ll, err := leaselocker.NewLeaseLocker(r.RestConfig, leaseLockerNSN, orLookupKey.String())
280+
if err != nil {
281+
return nil, ctrl.Result{}, err
282+
}
283+
284+
lockCtx, cancel := context.WithCancel(ctx)
285+
defer cancel()
286+
287+
// create lock
288+
locked := ll.TryLock(lockCtx)
289+
if !locked {
290+
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", ipRangeClaim.Spec.ParentPrefix))
291+
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
292+
ipRangeClaim.Spec.ParentPrefix)
293+
return ll, ctrl.Result{}, nil
294+
}
295+
debugLogger.Info(fmt.Sprintf("successfully locked parent prefix %s", ipRangeClaim.Spec.ParentPrefix))
296+
297+
return ll, ctrl.Result{}, nil
298+
}
299+
300+
func (r *IpRangeReconciler) updateLastMetadataAnnotation(ctx context.Context, annotations map[string]string, o *netboxv1.IpRange, accessor apismeta.MetadataAccessor) error {
301+
// update lastIpRangeMetadata annotation
302+
if annotations == nil {
303+
annotations = make(map[string]string)
304+
}
305+
306+
if len(o.Spec.CustomFields) > 0 {
307+
lastIpRangeMetadata, err := json.Marshal(o.Spec.CustomFields)
308+
if err != nil {
309+
return fmt.Errorf("failed to marshal lastIpRangeMetadata annotation: %w", err)
310+
}
311+
312+
annotations[LastIpRangeMetadataAnnotationName] = string(lastIpRangeMetadata)
313+
} else {
314+
annotations[LastIpRangeMetadataAnnotationName] = "{}"
315+
}
316+
317+
err := accessor.SetAnnotations(o, annotations)
318+
if err != nil {
319+
return err
320+
}
321+
322+
// update object to store lastIpRangeMetadata annotation
323+
if err := r.Update(ctx, o); err != nil {
324+
return err
325+
}
326+
327+
return nil
328+
}
329+
330+
func (r *IpRangeReconciler) removeFinalizer(ctx context.Context, o *netboxv1.IpRange) (ctrl.Result, error) {
331+
debugLogger := log.FromContext(ctx).V(4)
332+
if controllerutil.ContainsFinalizer(o, IpRangeFinalizerName) {
333+
debugLogger.Info("removing the finalizer")
334+
controllerutil.RemoveFinalizer(o, IpRangeFinalizerName)
335+
if err := r.Update(ctx, o); err != nil {
336+
return ctrl.Result{}, err
337+
}
338+
}
339+
340+
return ctrl.Result{}, nil
341+
}

0 commit comments

Comments
 (0)