Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIDRPool 1/x] Add CIDRPool CRD definition and validation logic #41

Merged
merged 1 commit into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")}
}
ykulazhenkov marked this conversation as resolved.
Show resolved Hide resolved
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
Loading