diff --git a/api/v1alpha1/cidrpool_test.go b/api/v1alpha1/cidrpool_test.go new file mode 100644 index 0000000..14aeeb4 --- /dev/null +++ b/api/v1alpha1/cidrpool_test.go @@ -0,0 +1,413 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + gomegaTypes "github.com/onsi/gomega/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/Mellanox/nvidia-k8s-ipam/api/v1alpha1" +) + +// validatePoolAndCheckErr runs validation for the allocation and checks that the result of +// the validation matches the expected result. +// if isValid is false, optional errMatcher can be provided to validate value of the error +func validatePoolAndCheckErr(pool *v1alpha1.CIDRPool, isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + errList := pool.Validate() + if isValid { + ExpectWithOffset(1, errList).To(BeEmpty()) + return + } + ExpectWithOffset(1, errList).NotTo(BeEmpty()) + if len(errMatcher) > 0 { + ExpectWithOffset(1, errList.ToAggregate().Error()).To(And(errMatcher...)) + } +} + +func getUintPtr(i uint) *uint { + return &i +} + +var _ = Describe("CIDRPool", func() { + It("Valid IPv4 pool", func() { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + GatewayIndex: getUintPtr(100), + Exclusions: []v1alpha1.ExcludeRange{ + {StartIP: "192.168.0.10", EndIP: "192.168.0.20"}, + {StartIP: "192.168.0.25", EndIP: "192.168.0.25"}, + }, + StaticAllocations: []v1alpha1.CIDRPoolStaticAllocation{ + {NodeName: "node1", Prefix: "192.168.5.0/24", Gateway: "192.168.5.10"}, + {NodeName: "node2", Prefix: "192.168.6.0/24"}, + {Prefix: "192.168.7.0/24"}, + }, + NodeSelector: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{ + Key: "foo.bar", + Operator: corev1.NodeSelectorOpExists, + }}, + }}, + }, + }, + } + validatePoolAndCheckErr(&cidrPool, true) + }) + It("Valid IPv6 pool", func() { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "fdf8:6aef:d1fe::/48", + PerNodeNetworkPrefix: 120, + GatewayIndex: getUintPtr(5), + Exclusions: []v1alpha1.ExcludeRange{ + {StartIP: "fdf8:6aef:d1fe::5", EndIP: "fdf8:6aef:d1fe::5"}, + }, + StaticAllocations: []v1alpha1.CIDRPoolStaticAllocation{ + {NodeName: "node1", Prefix: "fdf8:6aef:d1fe::/120", Gateway: "fdf8:6aef:d1fe::15"}, + }, + NodeSelector: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{ + Key: "foo.bar", + Operator: corev1.NodeSelectorOpExists, + }}, + }}, + }, + }, + } + validatePoolAndCheckErr(&cidrPool, true) + }) + DescribeTable("CIDR", + func(cidr string, prefix uint, isValid bool) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: cidr, + PerNodeNetworkPrefix: prefix, + }} + validatePoolAndCheckErr(&cidrPool, isValid, ContainSubstring("spec.cidr")) + }, + Entry("empty", "", uint(30), false), + Entry("invalid value", "aaaa", uint(30), false), + Entry("/32", "192.168.1.1/32", uint(32), false), + Entry("/128", "2001:db8:3333:4444::0/128", uint(128), false), + Entry("valid ipv4", "192.168.1.0/24", uint(30), true), + Entry("valid ipv6", "2001:db8:3333:4444::0/64", uint(120), true), + ) + DescribeTable("PerNodeNetworkPrefix", + func(cidr string, prefix uint, isValid bool) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: cidr, + PerNodeNetworkPrefix: prefix, + }} + validatePoolAndCheckErr(&cidrPool, isValid, ContainSubstring("spec.perNodeNetworkPrefix")) + }, + Entry("not set", "192.168.0.0/16", uint(0), false), + Entry("larger than CIDR", "192.168.0.0/16", uint(8), false), + Entry("smaller than 31 for IPv4 pool", "192.168.0.0/16", uint(32), false), + Entry("smaller than 127 for IPv6 pool", "2001:db8:3333:4444::0/64", uint(128), false), + Entry("match CIDR prefix size - ipv4", "192.168.0.0/16", uint(16), true), + Entry("match CIDR prefix size - ipv6", "2001:db8:3333:4444::0/64", uint(64), true), + ) + DescribeTable("NodeSelector", + func(nodeSelector *corev1.NodeSelector, isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + NodeSelector: nodeSelector, + }, + } + validatePoolAndCheckErr(&cidrPool, isValid, errMatcher...) + }, + Entry("not set", nil, true), + Entry("valid", &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "foo.bar", Operator: "Exists"}}, + }}, + }, true), + Entry("unknown operation", &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "foo.bar", Operator: "unknown"}}, + }}, + }, false, ContainSubstring("spec.nodeSelectorTerms[0].matchExpressions[0].operator")), + ) + DescribeTable("GatewayIndex", + func(gatewayIndex uint, isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + GatewayIndex: &gatewayIndex, + }, + } + validatePoolAndCheckErr(&cidrPool, isValid, ContainSubstring("spec.gatewayIndex")) + }, + Entry("too large", uint(255), false), + Entry("index 1 is valid for point to point", uint(1), true), + Entry("index 2 is valid for point to point", uint(2), true), + ) + DescribeTable("Exclusions", + func(exclusions []v1alpha1.ExcludeRange, isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + Exclusions: exclusions, + }, + } + validatePoolAndCheckErr(&cidrPool, isValid, errMatcher...) + }, + Entry("valid", []v1alpha1.ExcludeRange{ + {StartIP: "192.168.0.10", EndIP: "192.168.0.20"}, + }, true), + Entry("startIP not set", []v1alpha1.ExcludeRange{ + {EndIP: "192.168.0.20"}, + }, false, ContainSubstring("spec.exclusions[0].startIP")), + Entry("endIP not set", []v1alpha1.ExcludeRange{ + {StartIP: "192.168.0.10"}, + }, false, ContainSubstring("spec.exclusions[0].endIP")), + Entry("not IPs", []v1alpha1.ExcludeRange{ + {StartIP: "aaa", EndIP: "bb"}, + {StartIP: "192.168.0.25", EndIP: "ccc"}, + }, false, + ContainSubstring("spec.exclusions[0].startIP"), + ContainSubstring("spec.exclusions[0].endIP"), + ContainSubstring("spec.exclusions[1].endIP")), + Entry("startIP is greater then endIP", []v1alpha1.ExcludeRange{ + {StartIP: "192.168.0.25", EndIP: "192.168.0.24"}, + }, false, ContainSubstring("spec.exclusions[0]")), + Entry("doesn't belong to cidr", []v1alpha1.ExcludeRange{ + {StartIP: "10.10.33.25", EndIP: "10.10.33.33"}, + }, false, ContainSubstring("spec.exclusions[0]")), + ) + DescribeTable("StaticAllocations", + func(staticAllocations []v1alpha1.CIDRPoolStaticAllocation, isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + cidrPool := v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + StaticAllocations: staticAllocations, + }, + } + validatePoolAndCheckErr(&cidrPool, isValid, errMatcher...) + }, + Entry("valid", []v1alpha1.CIDRPoolStaticAllocation{ + {Prefix: "192.168.0.0/24", Gateway: "192.168.0.1", NodeName: "node1"}}, true), + Entry("valid - no gateway", []v1alpha1.CIDRPoolStaticAllocation{ + {Prefix: "192.168.0.0/24", NodeName: "node1"}}, true), + Entry("valid - no node name", []v1alpha1.CIDRPoolStaticAllocation{ + {Prefix: "192.168.0.0/24"}}, true), + Entry("not a prefix", []v1alpha1.CIDRPoolStaticAllocation{{Prefix: "192.168.0.0"}}, false, + ContainSubstring("spec.staticAllocations[0].prefix")), + Entry("wrong prefix size", []v1alpha1.CIDRPoolStaticAllocation{{Prefix: "192.168.0.0/31"}}, false, + ContainSubstring("spec.staticAllocations[0].prefix")), + Entry("prefix is not part of the cidr", []v1alpha1.CIDRPoolStaticAllocation{{Prefix: "10.10.10.0/24"}}, false, + ContainSubstring("spec.staticAllocations[0].prefix")), + Entry("gateway is not an IP", []v1alpha1.CIDRPoolStaticAllocation{ + {Prefix: "192.168.0.0/24", Gateway: "foo"}}, false, + ContainSubstring("spec.staticAllocations[0].gateway")), + Entry("gateway is not in the allocated prefix", []v1alpha1.CIDRPoolStaticAllocation{ + {Prefix: "192.168.0.0/24", Gateway: "192.168.1.1"}}, false, + ContainSubstring("spec.staticAllocations[0].gateway")), + Entry("duplicate node names", []v1alpha1.CIDRPoolStaticAllocation{ + {NodeName: "nodeA", Prefix: "192.168.0.0/24"}, + {NodeName: "nodeA", Prefix: "192.168.1.0/24"}}, false, + ContainSubstring("spec.staticAllocations")), + Entry("duplicate prefixes", []v1alpha1.CIDRPoolStaticAllocation{ + {NodeName: "nodeA", Prefix: "192.168.1.0/24"}, + {NodeName: "nodeB", Prefix: "192.168.1.0/24"}}, false, + ContainSubstring("spec.staticAllocations")), + ) +}) + +// validateAllocationAndCheckErr runs validation for the allocation and checks that the result of +// the validation matches the expected result. +// if isValid is false, optional errMatcher can be provided to validate value of the error +func validateAllocationAndCheckErr(allocation *v1alpha1.CIDRPoolAllocation, pool *v1alpha1.CIDRPool, + isValid bool, errMatcher ...gomegaTypes.GomegaMatcher) { + errList := allocation.Validate(pool) + if isValid { + ExpectWithOffset(1, errList).To(BeEmpty()) + return + } + ExpectWithOffset(1, errList).NotTo(BeEmpty()) + if len(errMatcher) > 0 { + ExpectWithOffset(1, errList.ToAggregate().Error()).To(And(errMatcher...)) + } +} + +var _ = Describe("CIDRPoolAllocation", func() { + var ( + cidrPool *v1alpha1.CIDRPool + ) + BeforeEach(func() { + cidrPool = &v1alpha1.CIDRPool{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1alpha1.CIDRPoolSpec{ + CIDR: "192.168.0.0/16", + PerNodeNetworkPrefix: 24, + GatewayIndex: getUintPtr(100), + StaticAllocations: []v1alpha1.CIDRPoolStaticAllocation{ + {NodeName: "node1", Prefix: "192.168.1.0/24", Gateway: "192.168.1.10"}, + {NodeName: "node2", Prefix: "192.168.2.0/24"}, + }, + }, + Status: v1alpha1.CIDRPoolStatus{ + Allocations: []v1alpha1.CIDRPoolAllocation{{ + NodeName: "node3", + Prefix: "192.168.3.0/24", + Gateway: "192.168.3.100", + }}, + }, + } + }) + Context("Valid", func() { + It("new", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node4", + Prefix: "192.168.4.0/24", + Gateway: "192.168.4.100", + }, cidrPool, true) + }) + It("exist in status", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node3", + Prefix: "192.168.3.0/24", + Gateway: "192.168.3.100", + }, cidrPool, true) + }) + It("Valid - no gateway", func() { + cidrPool.Spec.GatewayIndex = nil + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node3", + Prefix: "192.168.3.0/24", + }, cidrPool, true) + }) + }) + Context("Invalid", func() { + It("node not specified", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + Prefix: "192.168.4.0/24", + Gateway: "192.168.4.100", + }, cidrPool, false, ContainSubstring("nodeName")) + }) + It("empty", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{}, + cidrPool, false, ContainSubstring("nodeName"), ContainSubstring("prefix")) + }) + It("conflict with static allocation - range mismatch", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node1", + Prefix: "192.168.33.0/24", + Gateway: "192.168.33.100", + }, cidrPool, false, + ContainSubstring("gateway"), + ContainSubstring("prefix"), + ContainSubstring("static allocation"), + ) + }) + It("conflict with static allocation - prefix allocated for different node", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node1", + Prefix: "192.168.2.0/24", + Gateway: "192.168.2.100", + }, cidrPool, false, + ContainSubstring("prefix"), + ContainSubstring("static allocation"), + ) + }) + It("conflict with static allocation - gateway mismatch", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node2", + Prefix: "192.168.2.0/24", + }, cidrPool, false, + ContainSubstring("gateway"), + ) + }) + It("conflict with static allocation - dynamic gw instead of static", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node1", + Prefix: "192.168.1.0/24", + Gateway: "192.168.1.100", + }, cidrPool, false, + ContainSubstring("gateway"), + ) + }) + It("conflicting allocation", func() { + cidrPool.Status.Allocations = append(cidrPool.Status.Allocations, v1alpha1.CIDRPoolAllocation{ + NodeName: "node4", + Prefix: "192.168.3.0/24", + }) + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node3", + Prefix: "192.168.3.0/24", + Gateway: "192.168.3.100", + }, cidrPool, false, + ContainSubstring("conflicting allocation"), + ) + }) + It("gateway mismatch", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node5", + Prefix: "192.168.5.0/24", + }, cidrPool, false, + ContainSubstring("gateway"), + ) + }) + It("prefix has host bits set", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node5", + Prefix: "192.168.5.1/24", + Gateway: "192.168.5.100", + }, cidrPool, false, + ContainSubstring("prefix"), + ) + }) + It("wrong prefix size", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node5", + Prefix: "192.168.5.0/26", + Gateway: "192.168.5.100", + }, cidrPool, false, + ContainSubstring("prefix"), + ) + }) + It("wrong prefix", func() { + validateAllocationAndCheckErr(&v1alpha1.CIDRPoolAllocation{ + NodeName: "node5", + Prefix: "10.10.5.0/24", + Gateway: "10.10.5.100", + }, cidrPool, false, + ContainSubstring("prefix"), + ) + }) + }) + +}) diff --git a/api/v1alpha1/cidrpool_type.go b/api/v1alpha1/cidrpool_type.go new file mode 100644 index 0000000..53c7557 --- /dev/null +++ b/api/v1alpha1/cidrpool_type.go @@ -0,0 +1,91 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="CIDR",type="string",JSONPath=`.spec.cidr` +// +kubebuilder:printcolumn:name="Gateway index",type="string",JSONPath=`.spec.gatewayIndex` +// +kubebuilder:printcolumn:name="Per Node Network Prefix",type="integer",JSONPath=`.spec.perNodeNetworkPrefix` + +// CIDRPool contains configuration for CIDR pool +type CIDRPool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec CIDRPoolSpec `json:"spec"` + Status CIDRPoolStatus `json:"status,omitempty"` +} + +// CIDRPoolSpec contains configuration for CIDR pool +type CIDRPoolSpec struct { + // pool CIDR block which will be split to smaller prefixes(size is define in perNodeNetworkPrefix) + // and distributed between matching nodes + CIDR string `json:"cidr"` + // use IP with this index from the host prefix as a gateway, skip gateway configuration if the value not set + GatewayIndex *uint `json:"gatewayIndex,omitempty"` + // size of the network prefix for each host, the network defined in "cidr" field will be split to multiple networks + // with this size. + PerNodeNetworkPrefix uint `json:"perNodeNetworkPrefix"` + // contains reserved IP addresses that should not be allocated by nv-ipam + Exclusions []ExcludeRange `json:"exclusions,omitempty"` + // static allocations for the pool + StaticAllocations []CIDRPoolStaticAllocation `json:"staticAllocations,omitempty"` + // selector for nodes, if empty match all nodes + NodeSelector *corev1.NodeSelector `json:"nodeSelector,omitempty"` +} + +// CIDRPoolStatus contains the IP prefixes allocated to nodes +type CIDRPoolStatus struct { + // prefixes allocations for Nodes + Allocations []CIDRPoolAllocation `json:"allocations"` +} + +// CIDRPoolStaticAllocation contains static allocation for a CIDR pool +type CIDRPoolStaticAllocation struct { + // name of the node for static allocation, can be empty in case if the prefix + // should be preallocated without assigning it for a specific node + NodeName string `json:"nodeName,omitempty"` + // gateway for the node + Gateway string `json:"gateway,omitempty"` + // statically allocated prefix + Prefix string `json:"prefix"` +} + +// CIDRPoolAllocation contains prefix allocated for a specific Node +type CIDRPoolAllocation struct { + // name of the node which owns this allocation + NodeName string `json:"nodeName"` + // gateway for the node + Gateway string `json:"gateway,omitempty"` + // allocated prefix + Prefix string `json:"prefix"` +} + +// +kubebuilder:object:root=true + +// CIDRPoolList contains a list of CIDRPool +type CIDRPoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CIDRPool `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CIDRPool{}, &CIDRPoolList{}) +} diff --git a/api/v1alpha1/cidrpool_validate.go b/api/v1alpha1/cidrpool_validate.go new file mode 100644 index 0000000..4e9348f --- /dev/null +++ b/api/v1alpha1/cidrpool_validate.go @@ -0,0 +1,274 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + "net" + + cniUtils "github.com/containernetworking/cni/pkg/utils" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// Validate implements validation for the object fields +func (r *CIDRPool) Validate() field.ErrorList { + errList := field.ErrorList{} + if err := cniUtils.ValidateNetworkName(r.Name); err != nil { + errList = append(errList, field.Invalid( + field.NewPath("metadata", "name"), r.Name, + "invalid CIDR pool name, should be compatible with CNI network name")) + } + errList = append(errList, r.validateCIDR()...) + if r.Spec.NodeSelector != nil { + errList = append(errList, validateNodeSelector(r.Spec.NodeSelector, field.NewPath("spec"))...) + } + return errList +} + +// validate IP configuration of the CIDR pool +func (r *CIDRPool) validateCIDR() field.ErrorList { + netIP, network, err := net.ParseCIDR(r.Spec.CIDR) + if err != nil { + return field.ErrorList{field.Invalid(field.NewPath("spec", "cidr"), r.Spec.CIDR, "is invalid cidr")} + } + if !netIP.Equal(network.IP) { + return field.ErrorList{field.Invalid(field.NewPath("spec", "cidr"), r.Spec.CIDR, "network prefix has host bits set")} + } + + setBits, bitsTotal := network.Mask.Size() + if setBits == bitsTotal { + return field.ErrorList{field.Invalid( + field.NewPath("spec", "cidr"), r.Spec.CIDR, "single IP prefixes are not supported")} + } + if r.Spec.PerNodeNetworkPrefix == 0 || + r.Spec.PerNodeNetworkPrefix >= uint(bitsTotal) || + r.Spec.PerNodeNetworkPrefix < uint(setBits) { + return field.ErrorList{field.Invalid( + field.NewPath("spec", "perNodeNetworkPrefix"), + r.Spec.PerNodeNetworkPrefix, "must be less or equal than network prefix size in the \"cidr\" field")} + } + + errList := field.ErrorList{} + firstNodePrefix := &net.IPNet{IP: network.IP, Mask: net.CIDRMask(int(r.Spec.PerNodeNetworkPrefix), bitsTotal)} + if r.Spec.GatewayIndex != nil && GetGatewayForSubnet(firstNodePrefix, *r.Spec.GatewayIndex) == "" { + errList = append(errList, field.Invalid( + field.NewPath("spec", "gatewayIndex"), + r.Spec.GatewayIndex, "gateway index is outside of the node prefix")) + } + errList = append(errList, validateExclusions(network, r.Spec.Exclusions, field.NewPath("spec"))...) + errList = append(errList, r.validateStaticAllocations(network)...) + return errList +} + +// validateStatic allocations: +// - entries should be uniq (nodeName, prefix) +// - prefix should have the right size +// - prefix should be part of the pool cidr +// - gateway should be part of the prefix +func (r *CIDRPool) validateStaticAllocations(cidr *net.IPNet) field.ErrorList { + errList := field.ErrorList{} + + nodes := map[string]uint{} + prefixes := map[string]uint{} + + _, parentCIDRTotalBits := cidr.Mask.Size() + + for i, alloc := range r.Spec.StaticAllocations { + if alloc.NodeName != "" { + nodes[alloc.NodeName]++ + } + netIP, nodePrefix, err := net.ParseCIDR(alloc.Prefix) + if err != nil { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("prefix"), alloc.Prefix, + "is not a valid network prefix")) + continue + } + if !netIP.Equal(nodePrefix.IP) { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("prefix"), alloc.Prefix, + "network prefix has host bits set")) + continue + } + + prefixes[nodePrefix.String()]++ + + if !cidr.Contains(nodePrefix.IP) { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("prefix"), alloc.Prefix, + "prefix is not part of the pool cidr")) + continue + } + + nodePrefixOnes, nodePrefixTotalBits := nodePrefix.Mask.Size() + if parentCIDRTotalBits != nodePrefixTotalBits { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("prefix"), alloc.Prefix, + "ip family doesn't match the pool cidr")) + continue + } + if nodePrefixOnes != int(r.Spec.PerNodeNetworkPrefix) { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("prefix"), alloc.Prefix, + "prefix size doesn't match spec.perNodeNetworkPrefix")) + continue + } + + if alloc.Gateway != "" { + gwIP := net.ParseIP(alloc.Gateway) + if len(gwIP) == 0 { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("gateway"), alloc.Gateway, + "is not a valid IP")) + continue + } + if !nodePrefix.Contains(gwIP) { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations").Index(i).Child("gateway"), alloc.Gateway, + "is outside of the node prefix")) + continue + } + } + } + for k, v := range nodes { + if v > 1 { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations"), r.Spec.StaticAllocations, + fmt.Sprintf("contains multiple entries for node %s", k))) + } + } + for k, v := range prefixes { + if v > 1 { + errList = append(errList, field.Invalid( + field.NewPath("spec", "staticAllocations"), r.Spec.StaticAllocations, + fmt.Sprintf("contains multiple entries for prefix %s", k))) + } + } + return errList +} + +// Validate checks that CIDRPoolAllocation is a valid allocation for provided pool, +// it is expected that provided CIDRPool is already validated +// +//nolint:gocyclo +func (a *CIDRPoolAllocation) Validate(pool *CIDRPool) field.ErrorList { + errList := field.ErrorList{} + if a.NodeName == "" { + errList = append(errList, field.Invalid( + field.NewPath("nodeName"), a.NodeName, "can't be empty")) + } + if a.Prefix == "" { + errList = append(errList, field.Invalid( + field.NewPath("prefix"), a.Prefix, "can't be empty")) + } + if len(errList) > 0 { + return errList + } + + netIP, prefixNetwork, err := net.ParseCIDR(a.Prefix) + if err != nil { + return field.ErrorList{field.Invalid( + field.NewPath("prefix"), a.Prefix, "is not a valid network prefix")} + } + if !netIP.Equal(prefixNetwork.IP) { + return field.ErrorList{field.Invalid(field.NewPath("prefix"), a.Prefix, "network prefix has host bits set")} + } + + computedGW := "" + if pool.Spec.GatewayIndex != nil { + computedGW = GetGatewayForSubnet(prefixNetwork, *pool.Spec.GatewayIndex) + } + + // check static allocations first + for _, staticAlloc := range pool.Spec.StaticAllocations { + errList := field.ErrorList{} + if a.NodeName == staticAlloc.NodeName { + if a.Prefix != staticAlloc.Prefix { + errList = append(errList, field.Invalid( + field.NewPath("prefix"), a.Prefix, + fmt.Sprintf("doesn't match prefix from static allocation %s", staticAlloc.Prefix))) + } + if staticAlloc.Gateway != "" && a.Gateway != staticAlloc.Gateway { + errList = append(errList, field.Invalid( + field.NewPath("gateway"), a.Gateway, + fmt.Sprintf("doesn't match gateway from static allocation %s", staticAlloc.Gateway))) + } + if staticAlloc.Gateway == "" { + if computedGW != "" && a.Gateway != computedGW { + errList = append(errList, field.Invalid( + field.NewPath("gateway"), a.Gateway, + fmt.Sprintf("doesn't match computed gateway %s", computedGW))) + } + if computedGW == "" && a.Gateway != "" { + errList = append(errList, field.Invalid( + field.NewPath("gateway"), a.Gateway, "gateway expected to be empty")) + } + } + if len(errList) != 0 { + return errList + } + // allocation match the static allocation, no need for extra validation, because it is + // expected that the pool.staticAllocations were already validated. + return nil + } + if a.Prefix == staticAlloc.Prefix { + return field.ErrorList{field.Invalid( + field.NewPath("prefix"), a.Prefix, + fmt.Sprintf("is statically allocated for different node: %s", staticAlloc.NodeName))} + } + } + + _, cidr, _ := net.ParseCIDR(pool.Spec.CIDR) + _, parentCIDRTotalBits := cidr.Mask.Size() + + if !cidr.Contains(prefixNetwork.IP) { + return field.ErrorList{field.Invalid( + field.NewPath("prefix"), a.Prefix, + "is not part of the pool cidr")} + } + nodePrefixOnes, nodePrefixTotalBits := prefixNetwork.Mask.Size() + if parentCIDRTotalBits != nodePrefixTotalBits { + return field.ErrorList{field.Invalid( + field.NewPath("prefix"), a.Prefix, + "ip family is not match with the pool cidr")} + } + if nodePrefixOnes != int(pool.Spec.PerNodeNetworkPrefix) { + return field.ErrorList{field.Invalid( + field.NewPath("prefix"), a.Prefix, + "prefix size doesn't match spec.perNodeNetworkPrefix")} + } + + if computedGW != "" && a.Gateway != computedGW { + return field.ErrorList{field.Invalid( + field.NewPath("gateway"), a.Gateway, + fmt.Sprintf("doesn't match computed gateway %s", computedGW))} + } + if computedGW == "" && a.Gateway != "" { + return field.ErrorList{field.Invalid( + field.NewPath("gateway"), a.Gateway, "gateway expected to be empty")} + } + + // check for conflicting entries (all field should be uniq) + alreadyFound := false + for _, e := range pool.Status.Allocations { + if (a.Gateway != "" && a.Gateway == e.Gateway) || a.Prefix == e.Prefix || a.NodeName == e.NodeName { + if alreadyFound { + return field.ErrorList{field.Invalid( + field.NewPath("status"), a, fmt.Sprintf("conflicting allocation found in the pool: %v", e))} + } + alreadyFound = true + } + } + return nil +} diff --git a/api/v1alpha1/common_type.go b/api/v1alpha1/common_type.go new file mode 100644 index 0000000..5b9fb96 --- /dev/null +++ b/api/v1alpha1/common_type.go @@ -0,0 +1,21 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1 + +// ExcludeRange contains range of IP addresses to exclude from allocation +// startIP and endIP are part of the ExcludeRange +type ExcludeRange struct { + StartIP string `json:"startIP"` + EndIP string `json:"endIP"` +} diff --git a/api/v1alpha1/helpers.go b/api/v1alpha1/helpers.go new file mode 100644 index 0000000..86b65eb --- /dev/null +++ b/api/v1alpha1/helpers.go @@ -0,0 +1,72 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1 + +import ( + "math/big" + "net" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/ip" +) + +// GetGatewayForSubnet returns computed gateway for subnet as string, index 0 means no gw +// Examples: +// - subnet 192.168.0.0/24, index = 0, gateway = "" (invalid config, can't use network address as gateway ) +// - subnet 192.168.0.0/24, index 1, gateway = 192.168.1.1 +// - subnet 192.168.0.0/24, index 10, gateway = 102.168.1.10 +// - subnet 192.168.1.2/31, index 0, gateway = 192.168.1.2 (point to point network can use network IP as a gateway) +// - subnet 192.168.33.0/24, index 900, gateway = "" (invalid config, index too large) +func GetGatewayForSubnet(subnet *net.IPNet, index uint) string { + setBits, bitsTotal := subnet.Mask.Size() + maxIndex := GetPossibleIPCount(subnet) + + if setBits >= bitsTotal-1 { + // point to point or single IP + maxIndex.Sub(maxIndex, big.NewInt(1)) + } else if index == 0 { + // index 0 can be used only for point to point or single IP networks + return "" + } + if maxIndex.Cmp(big.NewInt(int64(index))) < 0 { + // index too large + return "" + } + gwIP := ip.NextIPWithOffset(subnet.IP, int64(index)) + if gwIP == nil { + return "" + } + return gwIP.String() +} + +// GetPossibleIPCount returns count of IP addresses for hosts in the provided subnet. +// for IPv4: returns amount of IPs - 2 (subnet address and broadcast address) +// for IPv6: returns amount of IPs - 1 (subnet address) +// for point to point subnets(/31 and /127) returns 2 +func GetPossibleIPCount(subnet *net.IPNet) *big.Int { + setBits, bitsTotal := subnet.Mask.Size() + if setBits == bitsTotal { + return big.NewInt(1) + } + if setBits == bitsTotal-1 { + // point to point + return big.NewInt(2) + } + ipCount := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(int64(bitsTotal-setBits)), nil) + if subnet.IP.To4() != nil { + ipCount.Sub(ipCount, big.NewInt(2)) + } else { + ipCount.Sub(ipCount, big.NewInt(1)) + } + return ipCount +} diff --git a/api/v1alpha1/helpers_test.go b/api/v1alpha1/helpers_test.go new file mode 100644 index 0000000..a28df9a --- /dev/null +++ b/api/v1alpha1/helpers_test.go @@ -0,0 +1,71 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1_test + +import ( + "net" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/api/v1alpha1" +) + +var _ = Describe("Helpers", func() { + DescribeTable("GetGatewayForSubnet", + func(subnet string, index int, gw string) { + _, network, err := net.ParseCIDR(subnet) + Expect(err).NotTo(HaveOccurred()) + ret := v1alpha1.GetGatewayForSubnet(network, uint(index)) + Expect(ret).To(Equal(gw)) + }, + Entry("ipv4 - start range", "192.168.1.0/24", 1, "192.168.1.1"), + Entry("ipv4 - mid range", "192.168.1.0/24", 33, "192.168.1.33"), + Entry("ipv4 - end range", "192.168.1.0/24", 254, "192.168.1.254"), + Entry("ipv4 - out of range, can't use index 0", "192.168.1.0/24", 0, ""), + Entry("ipv4 - out of range, too big", "192.168.1.0/24", 255, ""), + Entry("ipv4 - single IP", "192.168.1.100/32", 0, "192.168.1.100"), + Entry("ipv4 - single IP, out of range", "192.168.1.100/32", 1, ""), + Entry("ipv4 - point to point, can use index 0", "192.168.1.0/31", 0, "192.168.1.0"), + Entry("ipv4 - point to point, can use index 1", "192.168.1.0/31", 1, "192.168.1.1"), + Entry("ipv4 - point to point, out of range", "192.168.1.0/31", 2, ""), + Entry("ipv6 - start range", "2001:db8:3333:4444::0/120", 1, "2001:db8:3333:4444::1"), + Entry("ipv6 - mid range", "2001:db8:3333:4444::0/120", 10, "2001:db8:3333:4444::a"), + Entry("ipv6 - end range", "2001:db8:3333:4444::0/120", 255, "2001:db8:3333:4444::ff"), + Entry("ipv6 - out of range, can't use index 0", "2001:db8:3333:4444::0/120", 0, ""), + Entry("ipv6 - out of range, too big", "2001:db8:3333:4444::0/120", 256, ""), + Entry("ipv6 - point to point, can use index 0", "2001:db8:3333:4444::0/127", 0, "2001:db8:3333:4444::"), + Entry("ipv6 - point to point, can use index 1", "2001:db8:3333:4444::0/127", 1, "2001:db8:3333:4444::1"), + Entry("ipv6 - point to point, out of range", "2001:db8:3333:4444::0/127", 2, ""), + ) + DescribeTable("GetPossibleIPCount", + func(subnet string, count int) { + _, network, err := net.ParseCIDR(subnet) + Expect(err).NotTo(HaveOccurred()) + ret := v1alpha1.GetPossibleIPCount(network) + Expect(ret).NotTo(BeNil()) + Expect(ret.IsUint64()).To(BeTrue()) + Expect(ret.Uint64()).To(Equal(uint64(count))) + }, + Entry("ipv4 /24", "192.168.0.0/24", 254), + Entry("ipv4 /25", "192.168.0.0/25", 126), + Entry("ipv4 /16", "192.168.0.0/16", 65534), + Entry("ipv4 /31", "192.20.20.0/31", 2), + Entry("ipv4 /32", "10.10.10.10/32", 1), + Entry("ipv6 /124", "2001:db8:85a3::8a2e:370:7334/120", 255), + Entry("ipv6 /96", "2001:db8:85a3::8a2e:370:7334/96", 4294967295), + Entry("ipv6 /127", "2001:db8:85a3::8a2e:370:7334/127", 2), + Entry("ipv6 /128", "2001:db8:85a3::8a2e:370:7334/128", 1), + ) +}) diff --git a/api/v1alpha1/ippool_validate.go b/api/v1alpha1/ippool_validate.go index 265dbaa..55dc784 100644 --- a/api/v1alpha1/ippool_validate.go +++ b/api/v1alpha1/ippool_validate.go @@ -14,7 +14,7 @@ package v1alpha1 import ( - "math" + "math/big" "net" cniUtils "github.com/containernetworking/cni/pkg/utils" @@ -42,10 +42,7 @@ func (r *IPPool) Validate() field.ErrorList { } if network != nil && r.Spec.PerNodeBlockSize >= 2 { - setBits, bitsTotal := network.Mask.Size() - // possibleIPs = net size - network address - broadcast - possibleIPs := int(math.Pow(2, float64(bitsTotal-setBits))) - 2 - if possibleIPs < r.Spec.PerNodeBlockSize { + if GetPossibleIPCount(network).Cmp(big.NewInt(int64(r.Spec.PerNodeBlockSize))) < 0 { // config is not valid even if only one node exist in the cluster errList = append(errList, field.Invalid( field.NewPath("spec", "perNodeBlockSize"), r.Spec.PerNodeBlockSize, diff --git a/api/v1alpha1/validate_exclusions.go b/api/v1alpha1/validate_exclusions.go new file mode 100644 index 0000000..8c42f7e --- /dev/null +++ b/api/v1alpha1/validate_exclusions.go @@ -0,0 +1,59 @@ +/* + Copyright 2024, NVIDIA CORPORATION & AFFILIATES + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + "net" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/ip" +) + +// validateExclusions validate exclusions: +// - startIP and endIPs should be valid ip addresses from subnet, +// - endIP should be equal or greater than startIP +// there is no need to validate that ranges have no overlaps +func validateExclusions(subnet *net.IPNet, exclusions []ExcludeRange, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for i, e := range exclusions { + startIP := net.ParseIP(e.StartIP) + if len(startIP) == 0 { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("exclusions").Index(i).Child("startIP"), e.StartIP, + "is invalid IP address")) + } else if !subnet.Contains(startIP) { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("exclusions").Index(i).Child("startIP"), e.StartIP, + fmt.Sprintf("is not part of the %s subnet", subnet.String()))) + } + endIP := net.ParseIP(e.EndIP) + if len(endIP) == 0 { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("exclusions").Index(i).Child("endIP"), e.EndIP, + "is invalid IP address")) + } else if !subnet.Contains(endIP) { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("exclusions").Index(i).Child("endIP"), e.EndIP, + fmt.Sprintf("is not part of the %s subnet", subnet.String()))) + } + if len(startIP) > 0 && len(endIP) > 0 && ip.Cmp(endIP, startIP) < 0 { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("exclusions").Index(i), e, + "endIP should be equal or greater than startIP")) + } + } + return allErrs +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f8b97d4..73b46d1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -37,6 +37,165 @@ func (in *Allocation) DeepCopy() *Allocation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPool) DeepCopyInto(out *CIDRPool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPool. +func (in *CIDRPool) DeepCopy() *CIDRPool { + if in == nil { + return nil + } + out := new(CIDRPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CIDRPool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPoolAllocation) DeepCopyInto(out *CIDRPoolAllocation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPoolAllocation. +func (in *CIDRPoolAllocation) DeepCopy() *CIDRPoolAllocation { + if in == nil { + return nil + } + out := new(CIDRPoolAllocation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPoolList) DeepCopyInto(out *CIDRPoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CIDRPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPoolList. +func (in *CIDRPoolList) DeepCopy() *CIDRPoolList { + if in == nil { + return nil + } + out := new(CIDRPoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CIDRPoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPoolSpec) DeepCopyInto(out *CIDRPoolSpec) { + *out = *in + if in.GatewayIndex != nil { + in, out := &in.GatewayIndex, &out.GatewayIndex + *out = new(uint) + **out = **in + } + if in.Exclusions != nil { + in, out := &in.Exclusions, &out.Exclusions + *out = make([]ExcludeRange, len(*in)) + copy(*out, *in) + } + if in.StaticAllocations != nil { + in, out := &in.StaticAllocations, &out.StaticAllocations + *out = make([]CIDRPoolStaticAllocation, len(*in)) + copy(*out, *in) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = new(v1.NodeSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPoolSpec. +func (in *CIDRPoolSpec) DeepCopy() *CIDRPoolSpec { + if in == nil { + return nil + } + out := new(CIDRPoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPoolStaticAllocation) DeepCopyInto(out *CIDRPoolStaticAllocation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPoolStaticAllocation. +func (in *CIDRPoolStaticAllocation) DeepCopy() *CIDRPoolStaticAllocation { + if in == nil { + return nil + } + out := new(CIDRPoolStaticAllocation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRPoolStatus) DeepCopyInto(out *CIDRPoolStatus) { + *out = *in + if in.Allocations != nil { + in, out := &in.Allocations, &out.Allocations + *out = make([]CIDRPoolAllocation, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRPoolStatus. +func (in *CIDRPoolStatus) DeepCopy() *CIDRPoolStatus { + if in == nil { + return nil + } + out := new(CIDRPoolStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExcludeRange) DeepCopyInto(out *ExcludeRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExcludeRange. +func (in *ExcludeRange) DeepCopy() *ExcludeRange { + if in == nil { + return nil + } + out := new(ExcludeRange) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPPool) DeepCopyInto(out *IPPool) { *out = *in diff --git a/deploy/crds/kustomization.yaml b/deploy/crds/kustomization.yaml index a9c6a2f..580b335 100644 --- a/deploy/crds/kustomization.yaml +++ b/deploy/crds/kustomization.yaml @@ -1,2 +1,3 @@ resources: - nv-ipam.nvidia.com_ippools.yaml + - nv-ipam.nvidia.com_cidrpools.yaml diff --git a/deploy/crds/nv-ipam.nvidia.com_cidrpools.yaml b/deploy/crds/nv-ipam.nvidia.com_cidrpools.yaml new file mode 100644 index 0000000..665b068 --- /dev/null +++ b/deploy/crds/nv-ipam.nvidia.com_cidrpools.yaml @@ -0,0 +1,218 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: cidrpools.nv-ipam.nvidia.com +spec: + group: nv-ipam.nvidia.com + names: + kind: CIDRPool + listKind: CIDRPoolList + plural: cidrpools + singular: cidrpool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.cidr + name: CIDR + type: string + - jsonPath: .spec.gatewayIndex + name: Gateway index + type: string + - jsonPath: .spec.perNodeNetworkPrefix + name: Per Node Network Prefix + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: CIDRPool contains configuration for CIDR pool + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: CIDRPoolSpec contains configuration for CIDR pool + properties: + cidr: + description: pool CIDR block which will be split to smaller prefixes(size + is define in perNodeNetworkPrefix) and distributed between matching + nodes + type: string + 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 + gatewayIndex: + description: use IP with this index from the host prefix as a gateway, + skip gateway configuration if the value not set + type: integer + nodeSelector: + description: selector for nodes, if empty match all nodes + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The terms + are ORed. + items: + description: A null or empty node selector term matches no objects. + The requirements of them are ANDed. The TopologySelectorTerm + type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements by node's + labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to a + set of values. Valid operators are In, NotIn, Exists, + DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. If the operator is Gt or Lt, + the values array must have a single element, which + will be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements by node's + fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to a + set of values. Valid operators are In, NotIn, Exists, + DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. If the operator is Gt or Lt, + the values array must have a single element, which + will be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + perNodeNetworkPrefix: + description: size of the network prefix for each host, the network + defined in "cidr" field will be split to multiple networks with + this size. + type: integer + staticAllocations: + description: static allocations for the pool + items: + description: CIDRPoolStaticAllocation contains static allocation + for a CIDR pool + properties: + gateway: + description: gateway for the node + type: string + nodeName: + description: name of the node for static allocation, can be + empty in case if the prefix should be preallocated without + assigning it for a specific node + type: string + prefix: + description: statically allocated prefix + type: string + required: + - prefix + type: object + type: array + required: + - cidr + - perNodeNetworkPrefix + type: object + status: + description: CIDRPoolStatus contains the IP prefixes allocated to nodes + properties: + allocations: + description: prefixes allocations for Nodes + items: + description: CIDRPoolAllocation contains prefix allocated for a + specific Node + properties: + gateway: + description: gateway for the node + type: string + nodeName: + description: name of the node which owns this allocation + type: string + prefix: + description: allocated prefix + type: string + required: + - nodeName + - prefix + type: object + type: array + required: + - allocations + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/examples/cidrpool-1.yaml b/examples/cidrpool-1.yaml new file mode 100644 index 0000000..cabf5e6 --- /dev/null +++ b/examples/cidrpool-1.yaml @@ -0,0 +1,22 @@ +apiVersion: nv-ipam.nvidia.com/v1alpha1 +kind: CIDRPool +metadata: + name: pool1 + namespace: kube-system +spec: + cidr: 192.168.0.0/16 + gatewayIndex: 1 + perNodeNetworkPrefix: 24 + exclusions: # optional + - startIP: 192.168.0.10 + endIP: 192.168.0.20 + staticAllocations: # optional + - nodeName: node-33 + prefix: 192.168.33.0/24 + gateway: 192.168.33.10 + - prefix: 192.168.1.0/24 + nodeSelector: # optional + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/worker + operator: Exists