From 9a60011e35a58d026e8e31814cbbbf845be6ab0e Mon Sep 17 00:00:00 2001 From: Yury Kulazhenkov Date: Tue, 11 Jun 2024 13:29:32 +0300 Subject: [PATCH] Add support for IP address exclusions to IPPool CRD Signed-off-by: Yury Kulazhenkov --- README.md | 8 +++++++ api/v1alpha1/ippool_test.go | 26 +++++++++++++++++++-- api/v1alpha1/ippool_type.go | 2 ++ api/v1alpha1/ippool_validate.go | 3 +++ api/v1alpha1/zz_generated.deepcopy.go | 5 ++++ deploy/crds/nv-ipam.nvidia.com_ippools.yaml | 16 +++++++++++++ examples/ippool-1.yaml | 3 +++ pkg/ipam-node/controllers/ippool/ippool.go | 16 ++++++++----- 8 files changed, 71 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0951757..b176f2d 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,9 @@ spec: subnet: 192.168.0.0/16 perNodeBlockSize: 100 gateway: 192.168.0.1 + exclusions: # optional + - startIP: 192.168.0.10 + endIP: 192.168.0.20 nodeSelector: nodeSelectorTerms: - matchExpressions: @@ -374,6 +377,11 @@ spec: * `subnet`: IP Subnet of the pool. * `gateway` (optional): Gateway IP of the subnet. * `perNodeBlockSize`: the number of IPs of IP Blocks allocated to Nodes. + * `exclusions` (optional, list): contains reserved IP addresses that should not be allocated by nv-ipam node component. + + * `startIP`: start IP of the exclude range (inclusive). + * `endIP`: end IP of the exclude range (inclusive). + * `nodeSelector` (optional): A list of node selector terms. The terms are ORed. Each term can have a list of matchExpressions that are ANDed. Only the nodes that match the provided labels will get assigned IP Blocks for the defined pool. > __Notes:__ diff --git a/api/v1alpha1/ippool_test.go b/api/v1alpha1/ippool_test.go index 90bc4d6..c5ecf94 100644 --- a/api/v1alpha1/ippool_test.go +++ b/api/v1alpha1/ippool_test.go @@ -30,7 +30,10 @@ var _ = Describe("Validate", func() { Spec: v1alpha1.IPPoolSpec{ Subnet: "192.168.0.0/16", PerNodeBlockSize: 128, - Gateway: "192.168.0.1", + Exclusions: []v1alpha1.ExcludeRange{ + {StartIP: "192.168.0.100", EndIP: "192.168.0.110"}, + }, + Gateway: "192.168.0.1", NodeSelector: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{{ MatchExpressions: []corev1.NodeSelectorRequirement{{ @@ -49,7 +52,10 @@ var _ = Describe("Validate", func() { Spec: v1alpha1.IPPoolSpec{ Subnet: "2001:db8:3333:4444::0/64", PerNodeBlockSize: 1000, - Gateway: "2001:db8:3333:4444::1", + Exclusions: []v1alpha1.ExcludeRange{ + {StartIP: "2001:db8:3333:4444::3", EndIP: "2001:db8:3333:4444::4"}, + }, + Gateway: "2001:db8:3333:4444::1", }, } Expect(ipPool.Validate()).To(BeEmpty()) @@ -98,6 +104,22 @@ var _ = Describe("Validate", func() { ContainSubstring("spec.perNodeBlockSize"), ) }) + It("Invalid - exclusions not part of the subnet", func() { + ipPool := v1alpha1.IPPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.IPPoolSpec{ + Subnet: "192.168.0.0/24", + Exclusions: []v1alpha1.ExcludeRange{ + {StartIP: "10.10.10.10", EndIP: "10.10.10.20"}, + }, + PerNodeBlockSize: 10, + }, + } + Expect(ipPool.Validate().ToAggregate().Error()). + To( + ContainSubstring("spec.exclusions"), + ) + }) It("Invalid - gateway outside of the subnet", func() { ipPool := v1alpha1.IPPool{ ObjectMeta: metav1.ObjectMeta{Name: "test"}, diff --git a/api/v1alpha1/ippool_type.go b/api/v1alpha1/ippool_type.go index 3df519e..4a07738 100644 --- a/api/v1alpha1/ippool_type.go +++ b/api/v1alpha1/ippool_type.go @@ -39,6 +39,8 @@ type IPPoolSpec struct { // amount of IPs to allocate for each node, // must be less than amount of available IPs in the subnet PerNodeBlockSize int `json:"perNodeBlockSize"` + // contains reserved IP addresses that should not be allocated by nv-ipam + Exclusions []ExcludeRange `json:"exclusions,omitempty"` // gateway for the pool Gateway string `json:"gateway,omitempty"` // selector for nodes, if empty match all nodes diff --git a/api/v1alpha1/ippool_validate.go b/api/v1alpha1/ippool_validate.go index 55dc784..86baaf2 100644 --- a/api/v1alpha1/ippool_validate.go +++ b/api/v1alpha1/ippool_validate.go @@ -49,6 +49,9 @@ func (r *IPPool) Validate() field.ErrorList { "is larger then amount of IPs available in the subnet")) } } + if network != nil { + errList = append(errList, validateExclusions(network, r.Spec.Exclusions, field.NewPath("spec"))...) + } var parsedGW net.IP if r.Spec.Gateway != "" { parsedGW = net.ParseIP(r.Spec.Gateway) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 73b46d1..3927558 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -258,6 +258,11 @@ func (in *IPPoolList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in + if in.Exclusions != nil { + in, out := &in.Exclusions, &out.Exclusions + *out = make([]ExcludeRange, len(*in)) + copy(*out, *in) + } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = new(v1.NodeSelector) diff --git a/deploy/crds/nv-ipam.nvidia.com_ippools.yaml b/deploy/crds/nv-ipam.nvidia.com_ippools.yaml index c786905..8f73890 100644 --- a/deploy/crds/nv-ipam.nvidia.com_ippools.yaml +++ b/deploy/crds/nv-ipam.nvidia.com_ippools.yaml @@ -44,6 +44,22 @@ spec: spec: description: IPPoolSpec contains configuration for IP pool properties: + exclusions: + description: contains reserved IP addresses that should not be allocated + by nv-ipam + items: + description: ExcludeRange contains range of IP addresses to exclude + from allocation startIP and endIP are part of the ExcludeRange + properties: + endIP: + type: string + startIP: + type: string + required: + - endIP + - startIP + type: object + type: array gateway: description: gateway for the pool type: string diff --git a/examples/ippool-1.yaml b/examples/ippool-1.yaml index ab87176..16b2e59 100644 --- a/examples/ippool-1.yaml +++ b/examples/ippool-1.yaml @@ -6,6 +6,9 @@ metadata: spec: subnet: 192.168.0.0/16 perNodeBlockSize: 128 + exclusions: # optional + - startIP: 192.168.0.10 + endIP: 192.168.0.20 gateway: 192.168.0.1 nodeSelector: nodeSelectorTerms: diff --git a/pkg/ipam-node/controllers/ippool/ippool.go b/pkg/ipam-node/controllers/ippool/ippool.go index 5ed6609..de5ce03 100644 --- a/pkg/ipam-node/controllers/ippool/ippool.go +++ b/pkg/ipam-node/controllers/ippool/ippool.go @@ -52,15 +52,19 @@ func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } reqLog.Info("Notification on IPPool", "name", ipPool.Name) found := false - for _, alloc := range ipPool.Status.Allocations { if alloc.NodeName == r.NodeName { + exclusions := make([]pool.ExclusionRange, 0, len(ipPool.Spec.Exclusions)) + for _, e := range ipPool.Spec.Exclusions { + exclusions = append(exclusions, pool.ExclusionRange{StartIP: e.StartIP, EndIP: e.EndIP}) + } ipPool := &pool.Pool{ - Name: ipPool.Name, - Subnet: ipPool.Spec.Subnet, - Gateway: ipPool.Spec.Gateway, - StartIP: alloc.StartIP, - EndIP: alloc.EndIP, + Name: ipPool.Name, + Subnet: ipPool.Spec.Subnet, + Gateway: ipPool.Spec.Gateway, + StartIP: alloc.StartIP, + EndIP: alloc.EndIP, + Exclusions: exclusions, } r.PoolManager.UpdatePool(poolKey, ipPool) found = true