Skip to content

Commit f002bf8

Browse files
henrybear327CapiSSSbruelea
authored
Implement dynamic selection of parent prefix from a set of custom fields (#90)
Implement dynamic selection of parent prefix from a set of custom fields Notes: According to [2], the `oneOf` validation will only come in the next version Reference: [1] https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/ [2] https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-ratcheting Signed-off-by: Hoanganh.Mai <hoanganh.mai@swisscom.com> Signed-off-by: Chun-Hung Tseng <chun-hung.tseng@swisscom.com> Co-authored-by: Sergio <sergio.sancho.sanz6@gmail.com> Co-authored-by: bruelea <166021996+bruelea@users.noreply.github.com>
1 parent 5502c04 commit f002bf8

21 files changed

+696
-112
lines changed

ParentPrefixSelectorGuide.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# A guide of `ParentPrefixSelector` in `PrefixClaim`
2+
3+
There are 2 ways to make a Prefix claim:
4+
- provide a `parentPrefix`
5+
- provide a `parentPrefixSelector`
6+
7+
In this documentation, we will focus on the `parentPrefixSelector` only.
8+
9+
# CRD format
10+
11+
The following is a sample of utilizing the `parentPrefixSelector`:
12+
13+
```bash
14+
apiVersion: netbox.dev/v1
15+
kind: PrefixClaim
16+
metadata:
17+
labels:
18+
app.kubernetes.io/name: netbox-operator
19+
app.kubernetes.io/managed-by: kustomize
20+
name: prefixclaim-customfields-sample
21+
spec:
22+
tenant: "MY_TENANT"
23+
site: "DM-Akron"
24+
description: "some description"
25+
comments: "your comments"
26+
preserveInNetbox: true
27+
prefixLength: "/31"
28+
parentPrefixSelector:
29+
tenant: "MY_TENANT"
30+
site: "DM-Buffalo"
31+
environment: "Production"
32+
poolName: "Pool 1"
33+
```
34+
35+
The usage will be explained in the following sections.
36+
37+
## Notes on `Spec.tenant` and `Spec.site`
38+
39+
Please provide the *name*, not the *slug* value
40+
41+
## `parentPrefixSelector`
42+
43+
The `parentPrefixSelector` is a key-value map, where all the entries are of data type `<string-string>`.
44+
45+
The map contains a set of query conditions for selecting a set of prefixes that can be used as the parent prefix.
46+
47+
The query conditions will be chained by the AND operator, and exact match of the keys and values will be performed.
48+
49+
The fields that can be used as query conditions in the `parentPrefixSelector` are:
50+
- `tenant` and `site` (in lowercase characters)
51+
- these 2 fields are built-in fields from NetBox, so you do *not* need to create custom fields for them
52+
- please provide the *name*, not the *slug* value
53+
- if the entry for tenant or site is missing, it will *not* inherit from the tenant and site from the Spec
54+
- custom fields
55+
- the data types tested and supported so far are `string`, `integer`, and `boolean`
56+
- for `boolean` type, please use `true` and `false` as the value

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ Use Cases for this Restoration:
5858
- Disaster Recovery: In case the cluster is lost, IP Addresses can be restored with the IPAddressClaim only
5959
- Sticky IPs: Some services do not handle changes to IPs well. This ensures the IP/Prefix assigned to a Custom Resource is always the same.
6060

61+
# `ParentPrefixSelector` in `PrefixClaim`
62+
63+
Please read [ParentPrefixSelector guide] for more information!
64+
65+
[ParentPrefixSelector guide]: ./ParentPrefixSelectorGuide.md
66+
67+
6168
# Project Distribution
6269

6370
Following are the steps to build the installer and distribute this project to users.

api/v1/prefixclaim_types.go

+37-6
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,36 @@ import (
2424
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
2525

2626
// PrefixClaimSpec defines the desired state of PrefixClaim
27+
// TODO: The reason for using a workaround please see https://github.com/netbox-community/netbox-operator/pull/90#issuecomment-2402112475
2728
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.site) || has(self.site)", message="Site is required once set"
29+
// +kubebuilder:validation:XValidation:rule="(!has(self.parentPrefix) && has(self.parentPrefixSelector)) || (has(self.parentPrefix) && !has(self.parentPrefixSelector))"
2830
type PrefixClaimSpec struct {
31+
2932
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
3033
// Important: Run "make" to regenerate code after modifying this file
3134

32-
//+kubebuilder:validation:Required
3335
//+kubebuilder:validation:Format=cidr
3436
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'parentPrefix' is immutable"
35-
ParentPrefix string `json:"parentPrefix"`
37+
ParentPrefix string `json:"parentPrefix,omitempty"`
38+
39+
// The `parentPrefixSelector` is a key-value map, where all the entries are of data type `<string-string>`
40+
// The map contains a set of query conditions for selecting a set of prefixes that can be used as the parent prefix
41+
// The query conditions will be chained by the AND operator, and exact match of the keys and values will be performed
42+
// 2 built-in fields, namely `tenant` and `site`, along with custom fields, can be used
43+
// For more information, please see ParentPrefixSelectorGuide.md
44+
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'parentPrefixSelector' is immutable"
45+
ParentPrefixSelector map[string]string `json:"parentPrefixSelector,omitempty"`
3646

3747
//+kubebuilder:validation:Required
3848
//+kubebuilder:validation:Pattern=`^\/[0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]$`
3949
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'prefixLength' is immutable"
4050
PrefixLength string `json:"prefixLength"`
4151

52+
// Use the `name` value instead of the `slug` value
4253
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'site' is immutable"
4354
Site string `json:"site,omitempty"`
4455

56+
// Use the `name` value instead of the `slug` value
4557
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'tenant' is immutable"
4658
Tenant string `json:"tenant,omitempty"`
4759

@@ -58,10 +70,15 @@ type PrefixClaimSpec struct {
5870
type PrefixClaimStatus struct {
5971
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
6072
// Important: Run "make" to regenerate code after modifying this file
61-
// Prefix status: container, active, reserved , deprecated
62-
Prefix string `json:"prefix,omitempty"`
63-
PrefixName string `json:"prefixName,omitempty"`
64-
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
73+
// Prefix status: container, active, reserved, deprecated
74+
75+
// Due to the fact that the parent prefix can be specified directly in `ParentPrefix` or selected from `ParentPrefixSelector`,
76+
// we use this field to store exactly which parent prefix we are using for all subsequent reconcile loop calls,
77+
// as Spec.ParentPrefix is an immutable field, we can't overwrite it
78+
SelectedParentPrefix string `json:"parentPrefix,omitempty"`
79+
Prefix string `json:"prefix,omitempty"`
80+
PrefixName string `json:"prefixName,omitempty"`
81+
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
6582
}
6683

6784
// +kubebuilder:object:root=true
@@ -120,3 +137,17 @@ var ConditionPrefixAssignedFalse = metav1.Condition{
120137
Reason: "PrefixCRNotCreated",
121138
Message: "Failed to fetch new Prefix from NetBox",
122139
}
140+
141+
var ConditionParentPrefixSelectedTrue = metav1.Condition{
142+
Type: "ParentPrefixSelected",
143+
Status: "True",
144+
Reason: "ParentPrefixSelected",
145+
Message: "The parent prefix was selected successfully",
146+
}
147+
148+
var ConditionParentPrefixSelectedFalse = metav1.Condition{
149+
Type: "ParentPrefixSelected",
150+
Status: "False",
151+
Reason: "ParentPrefixNotSelected",
152+
Message: "The parent prefix was not able to be selected",
153+
}

api/v1/zz_generated.deepcopy.go

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/netbox.dev_prefixclaims.yaml

+26-6
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ spec:
5252
metadata:
5353
type: object
5454
spec:
55-
description: PrefixClaimSpec defines the desired state of PrefixClaim
55+
description: |-
56+
PrefixClaimSpec defines the desired state of PrefixClaim
57+
TODO: The reason for using a workaround please see https://github.com/netbox-community/netbox-operator/pull/90#issuecomment-2402112475
5658
properties:
5759
comments:
5860
type: string
@@ -68,6 +70,19 @@ spec:
6870
x-kubernetes-validations:
6971
- message: Field 'parentPrefix' is immutable
7072
rule: self == oldSelf
73+
parentPrefixSelector:
74+
additionalProperties:
75+
type: string
76+
description: |-
77+
The `parentPrefixSelector` is a key-value map, where all the entries are of data type `<string-string>`
78+
The map contains a set of query conditions for selecting a set of prefixes that can be used as the parent prefix
79+
The query conditions will be chained by the AND operator, and exact match of the keys and values will be performed
80+
2 built-in fields, namely `tenant` and `site`, along with custom fields, can be used
81+
For more information, please see ParentPrefixSelectorGuide.md
82+
type: object
83+
x-kubernetes-validations:
84+
- message: Field 'parentPrefixSelector' is immutable
85+
rule: self == oldSelf
7186
prefixLength:
7287
pattern: ^\/[0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]$
7388
type: string
@@ -77,22 +92,25 @@ spec:
7792
preserveInNetbox:
7893
type: boolean
7994
site:
95+
description: Use the `name` value instead of the `slug` value
8096
type: string
8197
x-kubernetes-validations:
8298
- message: Field 'site' is immutable
8399
rule: self == oldSelf
84100
tenant:
101+
description: Use the `name` value instead of the `slug` value
85102
type: string
86103
x-kubernetes-validations:
87104
- message: Field 'tenant' is immutable
88105
rule: self == oldSelf
89106
required:
90-
- parentPrefix
91107
- prefixLength
92108
type: object
93109
x-kubernetes-validations:
94110
- message: Site is required once set
95111
rule: '!has(oldSelf.site) || has(self.site)'
112+
- rule: (!has(self.parentPrefix) && has(self.parentPrefixSelector)) ||
113+
(has(self.parentPrefix) && !has(self.parentPrefixSelector))
96114
status:
97115
description: PrefixClaimStatus defines the observed state of PrefixClaim
98116
properties:
@@ -165,11 +183,13 @@ spec:
165183
- type
166184
type: object
167185
type: array
168-
prefix:
186+
parentPrefix:
169187
description: |-
170-
INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
171-
Important: Run "make" to regenerate code after modifying this file
172-
Prefix status: container, active, reserved , deprecated
188+
Due to the fact that the parent prefix can be specified directly in `ParentPrefix` or selected from `ParentPrefixSelector`,
189+
we use this field to store exactly which parent prefix we are using for all subsequent reconcile loop calls,
190+
as Spec.ParentPrefix is an immutable field, we can't overwrite it
191+
type: string
192+
prefix:
173193
type: string
174194
prefixName:
175195
type: string

config/samples/kustomization.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ resources:
44
- netbox_v1_ipaddressclaim.yaml
55
- netbox_v1_prefix.yaml
66
- netbox_v1_prefixclaim.yaml
7+
- netbox_v1_prefixclaim_parentprefixselector_bool_int.yaml
8+
- netbox_v1_prefixclaim_parentprefixselector.yaml
79
#+kubebuilder:scaffold:manifestskustomizesamples
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
apiVersion: netbox.dev/v1
2+
kind: PrefixClaim
3+
metadata:
4+
labels:
5+
app.kubernetes.io/name: netbox-operator
6+
app.kubernetes.io/managed-by: kustomize
7+
name: prefixclaim-parentprefixselector-sample
8+
spec:
9+
tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value
10+
site: "DM-Akron" # Use the `name` value instead of the `slug` value
11+
description: "some description"
12+
comments: "your comments"
13+
preserveInNetbox: true
14+
prefixLength: "/31"
15+
parentPrefixSelector: # The keys and values are case-sensitive
16+
# if the entry for tenant or site is missing, it will *not* inherit from the tenant and site from the Spec
17+
tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value
18+
site: "DM-Buffalo" # Use the `name` value instead of the `slug` value
19+
20+
# custom fields of your interest
21+
environment: "Production"
22+
poolName: "Pool 1"
23+
# environment: "production"
24+
# poolName: "pool 3"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
apiVersion: netbox.dev/v1
2+
kind: PrefixClaim
3+
metadata:
4+
labels:
5+
app.kubernetes.io/name: netbox-operator
6+
app.kubernetes.io/managed-by: kustomize
7+
name: prefixclaim-parentprefixselector-bool-int-sample
8+
spec:
9+
tenant: "MY_TENANT" # Use the `name` value instead of the `slug` value
10+
site: "DM-Akron" # Use the `name` value instead of the `slug` value
11+
description: "some description"
12+
comments: "your comments"
13+
preserveInNetbox: true
14+
prefixLength: "/31"
15+
parentPrefixSelector: # The keys and values are case-sensitive
16+
# should return a prefix in 3.0.0.0/24
17+
environment: "Production"
18+
poolName: "Pool 1"
19+
# same condition as above, should return a prefix in 3.0.0.0/24
20+
# cfDataTypeBool: "true"
21+
# cfDataTypeInteger: "1"
22+
23+
# should return a prefix in 3.0.2.0/24
24+
# environment: "Development"
25+
# poolName: "Pool 1"
26+
# same condition as above, should return a prefix in 3.0.0.0/24
27+
# cfDataTypeBool: "false"
28+
# cfDataTypeInteger: "2"

internal/controller/ipaddress_controller.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,8 @@ func (r *IpAddressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
147147
// create lock
148148
locked := ll.TryLock(lockCtx)
149149
if !locked {
150-
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", ipAddressClaim.Spec.ParentPrefix))
151-
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
152-
ipAddressClaim.Spec.ParentPrefix)
150+
errorMsg := fmt.Sprintf("failed to lock parent prefix %s", ipAddressClaim.Spec.ParentPrefix)
151+
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", errorMsg)
153152
return ctrl.Result{
154153
RequeueAfter: 2 * time.Second,
155154
}, nil

internal/controller/ipaddressclaim_controller.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"time"
2424

2525
netboxv1 "github.com/netbox-community/netbox-operator/api/v1"
26-
"github.com/netbox-community/netbox-operator/pkg/config"
2726
"github.com/netbox-community/netbox-operator/pkg/netbox/api"
2827
"github.com/netbox-community/netbox-operator/pkg/netbox/models"
2928
"github.com/swisscom/leaselocker"
@@ -111,9 +110,8 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
111110
locked := ll.TryLock(lockCtx)
112111
if !locked {
113112
// lock for parent prefix was not available, rescheduling
114-
logger.Info(fmt.Sprintf("failed to lock parent prefix %s", o.Spec.ParentPrefix))
115-
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", "failed to lock parent prefix %s",
116-
o.Spec.ParentPrefix)
113+
errorMsg := fmt.Sprintf("failed to lock parent prefix %s", o.Spec.ParentPrefix)
114+
r.Recorder.Eventf(o, corev1.EventTypeWarning, "FailedToLockParentPrefix", errorMsg)
117115
return ctrl.Result{
118116
RequeueAfter: 2 * time.Second,
119117
}, nil
@@ -122,7 +120,7 @@ func (r *IpAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
122120

123121
// 4. try to reclaim ip address
124122
h := generateIpAddressRestorationHash(o)
125-
ipAddressModel, err := r.NetboxClient.RestoreExistingIpByHash(config.GetOperatorConfig().NetboxRestorationHashFieldName, h)
123+
ipAddressModel, err := r.NetboxClient.RestoreExistingIpByHash(h)
126124
if err != nil {
127125
if setConditionErr := r.SetConditionAndCreateEvent(ctx, o, netboxv1.ConditionIpAssignedFalse, corev1.EventTypeWarning, err.Error()); setConditionErr != nil {
128126
return ctrl.Result{}, fmt.Errorf("error updating status: %w, looking up ip by hash failed: %w", setConditionErr, err)

0 commit comments

Comments
 (0)