Skip to content

Commit

Permalink
Add support for small allocation for IPPool CR
Browse files Browse the repository at this point in the history
- Add support for /31(/127) and /32(/128) subnets
- Add single IP allocations
  • Loading branch information
ykulazhenkov committed Oct 14, 2024
1 parent 5dfe9aa commit 5c81c89
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 31 deletions.
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
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 allocated the gateway,
// add a "virtual" allocatin 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
162 changes: 150 additions & 12 deletions pkg/ipam-controller/allocator/allocator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
testPoolName1 = "pool1"
testPoolName2 = "pool2"
testPerNodeBlockCount1 = 15
testPerNodeBlockCount2 = 10
testPerNodeBlockCount2 = 1
)

func getPool1() *ipamv1alpha1.IPPool {
Expand All @@ -54,7 +54,7 @@ func getPool2() *ipamv1alpha1.IPPool {
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "172.16.0.0/16",
PerNodeBlockSize: testPerNodeBlockCount2,
Gateway: "172.16.0.1"},
Gateway: "172.16.0.3"},
}
}

Expand All @@ -79,7 +79,7 @@ var _ = Describe("Allocator", func() {
Expect(node1AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.1"))
Expect(node1AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.15"))
Expect(node1AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.1"))
Expect(node1AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.10"))
Expect(node1AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.1"))

node1AllocSecondCall, err := pa1.AllocateFromPool(ctx, testNodeName1)
Expect(err).NotTo(HaveOccurred())
Expand All @@ -95,26 +95,26 @@ var _ = Describe("Allocator", func() {
Expect(err).NotTo(HaveOccurred())
Expect(node2AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.16"))
Expect(node2AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.30"))
Expect(node2AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.11"))
Expect(node2AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.20"))
Expect(node2AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.2"))
Expect(node2AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.2"))

node3AllocPool1, err := pa1.AllocateFromPool(ctx, testNodeName3)
Expect(err).NotTo(HaveOccurred())
node3AllocPool2, err := pa2.AllocateFromPool(ctx, testNodeName3)
Expect(err).NotTo(HaveOccurred())
Expect(node3AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.31"))
Expect(node3AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.45"))
Expect(node3AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.21"))
Expect(node3AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.30"))
Expect(node3AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.4"))
Expect(node3AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.4"))

node4AllocPool1, err := pa1.AllocateFromPool(ctx, testNodeName4)
Expect(err).NotTo(HaveOccurred())
node4AllocPool2, err := pa2.AllocateFromPool(ctx, testNodeName4)
Expect(err).NotTo(HaveOccurred())
Expect(node4AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.46"))
Expect(node4AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.60"))
Expect(node4AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.31"))
Expect(node4AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.40"))
Expect(node4AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.5"))
Expect(node4AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.5"))

// deallocate for node3 and node1
pa1.Deallocate(ctx, testNodeName1)
Expand All @@ -130,16 +130,16 @@ var _ = Describe("Allocator", func() {
Expect(node3AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.1"))
Expect(node3AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.15"))
Expect(node3AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.1"))
Expect(node3AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.10"))
Expect(node3AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.1"))

node1AllocPool1, err = pa1.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
node1AllocPool2, err = pa2.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1AllocPool1.StartIP.String()).To(BeEquivalentTo("192.168.0.31"))
Expect(node1AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.45"))
Expect(node1AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.21"))
Expect(node1AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.30"))
Expect(node1AllocPool2.StartIP.String()).To(BeEquivalentTo("172.16.0.4"))
Expect(node1AllocPool2.EndIP.String()).To(BeEquivalentTo("172.16.0.4"))
})

It("Deallocate from pool", func() {
Expand Down Expand Up @@ -236,6 +236,144 @@ var _ = Describe("Allocator", func() {
Expect(node2AllocPool1.EndIP.String()).To(BeEquivalentTo("192.168.0.30"))
})

It("Load single IP range", func() {
pool2 := getPool2()
pool2.Status = ipamv1alpha1.IPPoolStatus{
Allocations: []ipamv1alpha1.Allocation{
{
NodeName: testNodeName1,
StartIP: "172.16.0.1",
EndIP: "172.16.0.1",
},
{
// should discard, overlaps with GW
NodeName: testNodeName2,
StartIP: "172.16.0.3",
EndIP: "172.16.0.3",
},
{
NodeName: testNodeName3,
StartIP: "172.16.0.4",
EndIP: "172.16.0.4",
},
},
}
selectedNodes := sets.New(testNodeName1, testNodeName2)
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool2, selectedNodes)
node1AllocPool, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1AllocPool.StartIP.String()).To(BeEquivalentTo("172.16.0.1"))
Expect(node1AllocPool.EndIP.String()).To(BeEquivalentTo("172.16.0.1"))
node2AllocPool, err := a.AllocateFromPool(ctx, testNodeName2)
Expect(err).ToNot(HaveOccurred())
// should get the new IP
Expect(node2AllocPool.StartIP.String()).To(BeEquivalentTo("172.16.0.2"))
Expect(node2AllocPool.EndIP.String()).To(BeEquivalentTo("172.16.0.2"))
// should get IP from the status
node3AllocPool, err := a.AllocateFromPool(ctx, testNodeName3)
Expect(err).ToNot(HaveOccurred())
Expect(node3AllocPool.StartIP.String()).To(BeEquivalentTo("172.16.0.4"))
Expect(node3AllocPool.EndIP.String()).To(BeEquivalentTo("172.16.0.4"))
})

Context("small pools", func() {
It("/32 pool - can allocate if no gw", func() {
pool := &ipamv1alpha1.IPPool{
ObjectMeta: v1.ObjectMeta{Name: "small-pool"},
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "10.10.10.10/32",
PerNodeBlockSize: 1,
},
}
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool, sets.New(testNodeName1, testNodeName2))
node1Alloc, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.10"))
Expect(node1Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.10"))
_, err = a.AllocateFromPool(ctx, testNodeName2)
Expect(errors.Is(err, allocator.ErrNoFreeRanges)).To(BeTrue())
})
It("/32 pool - can't allocate if gw set", func() {
pool := &ipamv1alpha1.IPPool{
ObjectMeta: v1.ObjectMeta{Name: "small-pool"},
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "10.10.10.10/32",
PerNodeBlockSize: 1,
Gateway: "10.10.10.10",
},
}
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool, sets.New(testNodeName1, testNodeName2))
_, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(errors.Is(err, allocator.ErrNoFreeRanges)).To(BeTrue())
})
It("/31 pool - can allocate 2 ips", func() {
pool := &ipamv1alpha1.IPPool{
ObjectMeta: v1.ObjectMeta{Name: "small-pool"},
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "10.10.10.10/31",
PerNodeBlockSize: 2,
},
}
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool, sets.New(testNodeName1, testNodeName2))
node1Alloc, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.10"))
Expect(node1Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.11"))
_, err = a.AllocateFromPool(ctx, testNodeName2)
Expect(errors.Is(err, allocator.ErrNoFreeRanges)).To(BeTrue())
})
It("/31 pool - can allocate 1 ip for 2 nodes", func() {
pool := &ipamv1alpha1.IPPool{
ObjectMeta: v1.ObjectMeta{Name: "small-pool"},
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "10.10.10.10/31",
PerNodeBlockSize: 1,
},
}
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool, sets.New(testNodeName1, testNodeName2))
node1Alloc, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.10"))
Expect(node1Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.10"))
node2Alloc, err := a.AllocateFromPool(ctx, testNodeName2)
Expect(err).ToNot(HaveOccurred())
Expect(node2Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.11"))
Expect(node2Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.11"))
})
It("load test", func () {
pool := &ipamv1alpha1.IPPool{
ObjectMeta: v1.ObjectMeta{Name: "small-pool"},
Spec: ipamv1alpha1.IPPoolSpec{
Subnet: "10.10.10.10/31",
PerNodeBlockSize: 1,
},
}
pool.Status = ipamv1alpha1.IPPoolStatus{
Allocations: []ipamv1alpha1.Allocation{
{
NodeName: testNodeName1,
StartIP: "10.10.10.10",
EndIP: "10.10.10.10",
},
{
NodeName: testNodeName2,
StartIP: "10.10.10.11",
EndIP: "10.10.10.11",
},
},
}
a := allocator.CreatePoolAllocatorFromIPPool(ctx, pool, sets.New(testNodeName1, testNodeName2))
node2Alloc, err := a.AllocateFromPool(ctx, testNodeName2)
Expect(err).ToNot(HaveOccurred())
Expect(node2Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.11"))
Expect(node2Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.11"))
node1Alloc, err := a.AllocateFromPool(ctx, testNodeName1)
Expect(err).ToNot(HaveOccurred())
Expect(node1Alloc.StartIP.String()).To(BeEquivalentTo("10.10.10.10"))
Expect(node1Alloc.EndIP.String()).To(BeEquivalentTo("10.10.10.10"))
})
})

It("ConfigureAndLoadAllocations - Data load test", func() {
getValidData := func() *allocator.AllocatedRange {
return &allocator.AllocatedRange{
Expand Down
2 changes: 1 addition & 1 deletion pkg/ipam-controller/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var _ = Describe("Config", func() {
})
It("Invalid pool: perNodeBlockSize too small", func() {
poolConfig := getValidPool()
poolConfig.PerNodeBlockSize = 1
poolConfig.PerNodeBlockSize = 0
cfg := &config.Config{Pools: map[string]config.PoolConfig{"pool1": poolConfig}}
Expect(cfg.Validate()).To(HaveOccurred())
})
Expand Down

0 comments on commit 5c81c89

Please sign in to comment.