Skip to content

Commit

Permalink
Merge pull request #41 from ykulazhenkov/pr-cidr-pool-api
Browse files Browse the repository at this point in the history
[CIDRPool 1/x] Add CIDRPool CRD definition and validation logic
  • Loading branch information
ykulazhenkov committed Jun 11, 2024
2 parents 18fc6a0 + bed2490 commit 3ddd30f
Show file tree
Hide file tree
Showing 12 changed files with 1,403 additions and 5 deletions.
413 changes: 413 additions & 0 deletions api/v1alpha1/cidrpool_test.go

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions api/v1alpha1/cidrpool_type.go
Original file line number Diff line number Diff line change
@@ -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{})
}
274 changes: 274 additions & 0 deletions api/v1alpha1/cidrpool_validate.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions api/v1alpha1/common_type.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading

0 comments on commit 3ddd30f

Please sign in to comment.