Skip to content

Commit

Permalink
Merge pull request #57 from ykulazhenkov/pr-single-ip-pools
Browse files Browse the repository at this point in the history
Add support for /32 (/128) networks and perNodeBlockSize 1
  • Loading branch information
ykulazhenkov authored Oct 15, 2024
2 parents f85a716 + 07ef882 commit 0db060c
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 67 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ spec:
> __Notes:__
>
> * pool name is composed of alphanumeric letters separated by dots(`.`) underscores(`_`) or hyphens(`-`).
> * `perNodeBlockSize` minimum size is 2.
> * `perNodeBlockSize` minimum size is 1.
> * `subnet` must be large enough to accommodate at least one `perNodeBlockSize` block of IPs.


Expand Down
8 changes: 4 additions & 4 deletions api/v1alpha1/cidrpool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ var _ = Describe("CIDRPool", func() {
},
Entry("empty", "", int32(30), false),
Entry("invalid value", "aaaa", int32(30), false),
Entry("/32", "192.168.1.1/32", int32(32), false),
Entry("/128", "2001:db8:3333:4444::0/128", int32(128), false),
Entry("/32", "192.168.1.1/32", int32(32), true),
Entry("/128", "2001:db8:3333:4444::0/128", int32(128), true),
Entry("valid ipv4", "192.168.1.0/24", int32(30), true),
Entry("valid ipv6", "2001:db8:3333:4444::0/64", int32(120), true),
)
Expand All @@ -203,8 +203,8 @@ var _ = Describe("CIDRPool", func() {
Entry("not set", "192.168.0.0/16", int32(0), false),
Entry("negative", "192.168.0.0/16", int32(-10), false),
Entry("larger than CIDR", "192.168.0.0/16", int32(8), false),
Entry("smaller than 31 for IPv4 pool", "192.168.0.0/16", int32(32), false),
Entry("smaller than 127 for IPv6 pool", "2001:db8:3333:4444::0/64", int32(128), false),
Entry("32 for IPv4 pool", "192.168.0.0/16", int32(32), true),
Entry("128 for IPv6 pool", "2001:db8:3333:4444::0/64", int32(128), true),
Entry("match CIDR prefix size - ipv4", "192.168.0.0/16", int32(16), true),
Entry("match CIDR prefix size - ipv6", "2001:db8:3333:4444::0/64", int32(64), true),
)
Expand Down
10 changes: 2 additions & 8 deletions api/v1alpha1/cidrpool_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ func (r *CIDRPool) validateCIDR() field.ErrorList {
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.GatewayIndex != nil && *r.Spec.GatewayIndex < 0 {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "gatewayIndex"), r.Spec.GatewayIndex, "must not be negative")}
Expand All @@ -75,9 +69,9 @@ func (r *CIDRPool) validateCIDR() field.ErrorList {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "perNodeNetworkPrefix"), r.Spec.PerNodeNetworkPrefix, "must not be negative")}
}

setBits, bitsTotal := network.Mask.Size()
if r.Spec.PerNodeNetworkPrefix == 0 ||
r.Spec.PerNodeNetworkPrefix >= int32(bitsTotal) ||
r.Spec.PerNodeNetworkPrefix > int32(bitsTotal) ||
r.Spec.PerNodeNetworkPrefix < int32(setBits) {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "perNodeNetworkPrefix"),
Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha1/ippool_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ func (r *IPPool) Validate() field.ErrorList {
field.NewPath("spec", "subnet"), r.Spec.Subnet, "is invalid subnet"))
}

if r.Spec.PerNodeBlockSize < 2 {
if r.Spec.PerNodeBlockSize < 1 {
errList = append(errList, field.Invalid(
field.NewPath("spec", "perNodeBlockSize"),
r.Spec.PerNodeBlockSize, "must be at least 2"))
r.Spec.PerNodeBlockSize, "must be at least 1"))
}

if network != nil && r.Spec.PerNodeBlockSize >= 2 {
if network != nil && r.Spec.PerNodeBlockSize >= 1 {
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(
Expand Down
12 changes: 12 additions & 0 deletions pkg/ip/cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ func IsBroadcast(ip net.IP, network *net.IPNet) bool {
if network.IP.To4() == nil {
return false
}
if IsPointToPointSubnet(network) || IsSingleIPSubnet(network) {
return false
}
if !network.Contains(ip) {
return false
}
Expand All @@ -153,8 +156,17 @@ func IsPointToPointSubnet(network *net.IPNet) bool {
return ones == maskLen-1
}

// IsSingleIPSubnet returns true if the network is a single IP subnet (/32 or /128)
func IsSingleIPSubnet(network *net.IPNet) bool {
ones, maskLen := network.Mask.Size()
return ones == maskLen
}

// LastIP returns the last IP of a subnet, excluding the broadcast if IPv4 (if not /31 net)
func LastIP(network *net.IPNet) net.IP {
if IsSingleIPSubnet(network) {
return network.IP
}
var end net.IP
for i := 0; i < len(network.IP); i++ {
end = append(end, network.IP[i]|^network.Mask[i])
Expand Down
66 changes: 66 additions & 0 deletions pkg/ip/cidr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ var _ = Describe("CIDR functions", func() {
testNet,
true,
},
{
net.ParseIP("192.168.0.10"),
func() *net.IPNet {
_, testNet, _ := net.ParseCIDR("192.168.0.10/32")
return testNet
}(),
false,
},
{
net.ParseIP("192.168.0.1"),
func() *net.IPNet {
_, testNet, _ := net.ParseCIDR("192.168.0.0/31")
return testNet
}(),
false,
},
}

for _, test := range testCases {
Expand Down Expand Up @@ -373,6 +389,30 @@ var _ = Describe("CIDR functions", func() {
Expect(gen().String()).To(Equal("::2/127"))
Expect(gen().String()).To(Equal("::4/127"))
})
It("valid - single IP IPv4 subnet", func() {
_, net, _ := net.ParseCIDR("192.168.0.0/16")
gen := GetSubnetGen(net, 32)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("192.168.0.0/32"))
Expect(gen().String()).To(Equal("192.168.0.1/32"))
Expect(gen().String()).To(Equal("192.168.0.2/32"))
})
It("valid - single IP IPv6 subnet", func() {
_, net, _ := net.ParseCIDR("2002:0:0:1234::/64")
gen := GetSubnetGen(net, 128)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("2002:0:0:1234::/128"))
Expect(gen().String()).To(Equal("2002:0:0:1234::1/128"))
Expect(gen().String()).To(Equal("2002:0:0:1234::2/128"))
})
It("valid - single IP IPv4 subnet, point to point network", func() {
_, net, _ := net.ParseCIDR("192.168.0.0/31")
gen := GetSubnetGen(net, 32)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("192.168.0.0/32"))
Expect(gen().String()).To(Equal("192.168.0.1/32"))
Expect(gen()).To(BeNil())
})
})
Context("IsPointToPointSubnet", func() {
It("/31", func() {
Expand All @@ -388,6 +428,24 @@ var _ = Describe("CIDR functions", func() {
Expect(IsPointToPointSubnet(network)).To(BeFalse())
})
})
Context("IsSingleIPSubnet", func() {
It("/32", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/32")
Expect(IsSingleIPSubnet(network)).To(BeTrue())
})
It("/128", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::1/128")
Expect(IsSingleIPSubnet(network)).To(BeTrue())
})
It("/24", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/24")
Expect(IsSingleIPSubnet(network)).To(BeFalse())
})
It("/31", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/31")
Expect(IsSingleIPSubnet(network)).To(BeFalse())
})
})
Context("LastIP", func() {
It("/31", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/31")
Expand All @@ -397,6 +455,14 @@ var _ = Describe("CIDR functions", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::0/127")
Expect(LastIP(network).String()).To(Equal("2002:0:0:1234::1"))
})
It("/32", func() {
_, network, _ := net.ParseCIDR("192.168.1.10/32")
Expect(LastIP(network).String()).To(Equal("192.168.1.10"))
})
It("/128", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::10/128")
Expect(LastIP(network).String()).To(Equal("2002:0:0:1234::10"))
})
It("/24", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/24")
Expect(LastIP(network).String()).To(Equal("192.168.1.254"))
Expand Down
55 changes: 40 additions & 15 deletions pkg/ipam-controller/allocator/allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@ func (pa *PoolAllocator) AllocateFromPool(ctx context.Context, node string) (*Al
return &existingAlloc, nil
}
allocations := pa.getAllocationsAsSlice()
// determine the first possible range for the subnet
var startIP net.IP
if len(allocations) == 0 || ip.Distance(pa.cfg.Subnet.IP, allocations[0].StartIP) > 2 {
// start allocations from the network address if there are no allocations or if the "hole" exist before
// the firs allocation
startIP = ip.NextIP(pa.cfg.Subnet.IP)
if pa.canUseNetworkAddress() {
startIP = pa.cfg.Subnet.IP
} else {
startIP = ip.NextIP(pa.cfg.Subnet.IP)
}
// check if the first possible range is already allocated, if so, search for "holes" or use the next subnet
if len(allocations) != 0 && allocations[0].StartIP.Equal(startIP) {
startIP = nil
for i := 0; i < len(allocations); i++ {
nextI := i + 1
// if last allocation in the list
Expand Down Expand Up @@ -122,6 +126,12 @@ func (pa *PoolAllocator) Deallocate(ctx context.Context, node string) {
}
}

// canUseNetworkAddress returns true if it is allowed to use network address in the node range
// it is allowed to use network address if the subnet is point to point of a single IP subnet
func (pa *PoolAllocator) canUseNetworkAddress() bool {
return ip.IsPointToPointSubnet(pa.cfg.Subnet) || ip.IsSingleIPSubnet(pa.cfg.Subnet)
}

// load loads range to the pool allocator with validation for conflicts
func (pa *PoolAllocator) load(ctx context.Context, nodeName string, allocRange AllocatedRange) error {
log := pa.getLog(ctx, pa.cfg).WithValues("node", nodeName)
Expand All @@ -147,29 +157,44 @@ func (pa *PoolAllocator) checkAllocation(allocRange AllocatedRange) error {
if !pa.cfg.Subnet.Contains(allocRange.StartIP) || !pa.cfg.Subnet.Contains(allocRange.EndIP) {
return fmt.Errorf("invalid allocation allocators: start or end IP is out of the subnet")
}

if ip.Cmp(allocRange.EndIP, allocRange.StartIP) <= 0 {
return fmt.Errorf("invalid allocation allocators: start IP must be less then end IP")
if ip.Cmp(allocRange.EndIP, allocRange.StartIP) < 0 {
return fmt.Errorf("invalid allocation allocators: start IP must be less or equal to end IP")
}

// check that StartIP of the range has valid offset.
// all ranges have same size, so we can simply check that (StartIP offset - 1) % pa.cfg.PerNodeBlockSize == 0
// -1 required because we skip network addressee (e.g. in 192.168.0.0/24, first allocation will be 192.168.0.1)
distanceFromNetworkStart := ip.Distance(pa.cfg.Subnet.IP, allocRange.StartIP)
if distanceFromNetworkStart < 1 ||
math.Mod(float64(distanceFromNetworkStart)-1, float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
// check that StartIP of the range has valid offset.
// all ranges have same size, so we can simply check that (StartIP offset) % pa.cfg.PerNodeBlockSize == 0
if pa.canUseNetworkAddress() {
if math.Mod(float64(distanceFromNetworkStart), float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
}
} else {
if distanceFromNetworkStart < 1 ||
// -1 required because we skip network address (e.g. in 192.168.0.0/24, first allocation will be 192.168.0.1)
math.Mod(float64(distanceFromNetworkStart)-1, float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
}
}
if ip.Distance(allocRange.StartIP, allocRange.EndIP) != int64(pa.cfg.PerNodeBlockSize)-1 {
return fmt.Errorf("ip count mismatch")
}
// for single IP ranges we need to discard allocation if it matches the gateway
if pa.cfg.PerNodeBlockSize == 1 && pa.cfg.Gateway != nil && allocRange.StartIP.Equal(pa.cfg.Gateway) {
return fmt.Errorf("gw can't be allocated when perNodeBlockSize is 1")
}
return nil
}

// return slice with allocated ranges.
// ranges are not overlap and are sorted, but there can be "holes" between ranges
func (pa *PoolAllocator) getAllocationsAsSlice() []AllocatedRange {
allocatedRanges := make([]AllocatedRange, 0, len(pa.allocations))
allocatedRanges := make([]AllocatedRange, 0, len(pa.allocations)+1)

if pa.cfg.PerNodeBlockSize == 1 && pa.cfg.Gateway != nil {
// in case if perNodeBlockSize is 1 we should not allocate the gateway,
// add a "virtual" allocation for the gateway if we detect that only 1 IP is requested per node,
// this allocation should never be exposed to the CR's status
allocatedRanges = append(allocatedRanges, AllocatedRange{StartIP: pa.cfg.Gateway, EndIP: pa.cfg.Gateway})
}
for _, a := range pa.allocations {
allocatedRanges = append(allocatedRanges, a)
}
Expand Down
Loading

0 comments on commit 0db060c

Please sign in to comment.