From 9eb06d5cb46c162a3ad6e55705b020e4159e4c94 Mon Sep 17 00:00:00 2001 From: Feruzjon Muyassarov Date: Thu, 15 Apr 2021 17:20:24 +0300 Subject: [PATCH] Fix issue with pre-allocated addresses In case of pre-allocated addresses, the claims were not given the proper prefix, gateway and dns server overrides. In addition to fixing this, some validations on the pool updates are added to verify that no addresses would be out of bonds. --- api/v1alpha1/ippool_webhook.go | 65 ++++++++ api/v1alpha1/ippool_webhook_test.go | 102 ++++++++++-- api/v1alpha1/utils.go | 126 +++++++++++++++ api/v1alpha1/utils_test.go | 190 ++++++++++++++++++++++ ipam/ippool_manager.go | 150 +++++------------- ipam/ippool_manager_test.go | 236 +++++++++------------------- 6 files changed, 574 insertions(+), 295 deletions(-) create mode 100644 api/v1alpha1/utils.go create mode 100644 api/v1alpha1/utils_test.go diff --git a/api/v1alpha1/ippool_webhook.go b/api/v1alpha1/ippool_webhook.go index e5eeaf0f..2ebe1c86 100644 --- a/api/v1alpha1/ippool_webhook.go +++ b/api/v1alpha1/ippool_webhook.go @@ -61,6 +61,29 @@ func (c *IPPool) ValidateUpdate(old runtime.Object) error { ), ) } + allocationOutOfBonds, inUseOutOfBonds := c.checkPoolBonds(oldM3ipp) + if len(allocationOutOfBonds) != 0 { + for _, address := range allocationOutOfBonds { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "preAllocations"), + address, + "is out of bonds of the pools given", + ), + ) + } + } + if len(inUseOutOfBonds) != 0 { + for _, address := range inUseOutOfBonds { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "pools"), + address, + "is in use but out of bonds of the pools given", + ), + ) + } + } if len(allErrs) == 0 { return nil @@ -68,6 +91,48 @@ func (c *IPPool) ValidateUpdate(old runtime.Object) error { return apierrors.NewInvalid(GroupVersion.WithKind("Metal3Data").GroupKind(), c.Name, allErrs) } +func (c *IPPool) checkPoolBonds(old *IPPool) ([]IPAddressStr, []IPAddressStr) { + allocationOutOfBonds := []IPAddressStr{} + inUseOutOfBonds := []IPAddressStr{} + for _, address := range c.Spec.PreAllocations { + inBonds := c.isAddressInBonds(address) + + if !inBonds { + allocationOutOfBonds = append(allocationOutOfBonds, address) + } + } + for _, address := range old.Status.Allocations { + inBonds := c.isAddressInBonds(address) + + if !inBonds { + inUseOutOfBonds = append(inUseOutOfBonds, address) + } + } + return allocationOutOfBonds, inUseOutOfBonds +} + +func (c *IPPool) isAddressInBonds(address IPAddressStr) bool { + inBonds := false + for _, pool := range c.Spec.Pools { + if inBonds { + break + } + index := 0 + for !inBonds { + allocatedAddress, err := GetIPAddress(pool, index) + if err != nil { + break + } + index++ + if allocatedAddress == address { + inBonds = true + break + } + } + } + return inBonds +} + // ValidateDelete implements webhook.Validator so a webhook will be registered for the type func (c *IPPool) ValidateDelete() error { return nil diff --git a/api/v1alpha1/ippool_webhook_test.go b/api/v1alpha1/ippool_webhook_test.go index 321938d1..a71d9aff 100644 --- a/api/v1alpha1/ippool_webhook_test.go +++ b/api/v1alpha1/ippool_webhook_test.go @@ -71,33 +71,100 @@ func TestIPPoolValidation(t *testing.T) { func TestIPPoolUpdateValidation(t *testing.T) { + startAddr := IPAddressStr("192.168.0.1") + endAddr := IPAddressStr("192.168.0.10") + tests := []struct { - name string - expectErr bool - newPool *IPPoolSpec - oldPool *IPPoolSpec + name string + expectErr bool + newPoolSpec *IPPoolSpec + oldPoolSpec *IPPoolSpec + oldPoolStatus IPPoolStatus }{ { - name: "should succeed when values and templates correct", - expectErr: false, - newPool: &IPPoolSpec{}, - oldPool: &IPPoolSpec{}, + name: "should succeed when values and templates correct", + expectErr: false, + newPoolSpec: &IPPoolSpec{}, + oldPoolSpec: &IPPoolSpec{}, }, { - name: "should fail when oldPool is nil", - expectErr: true, - newPool: &IPPoolSpec{}, - oldPool: nil, + name: "should fail when oldPoolSpec is nil", + expectErr: true, + newPoolSpec: &IPPoolSpec{}, + oldPoolSpec: nil, }, { name: "should fail when namePrefix value changes", expectErr: true, - newPool: &IPPoolSpec{ + newPoolSpec: &IPPoolSpec{ NamePrefix: "abcde", }, - oldPool: &IPPoolSpec{ + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + }, + { + name: "should succeed when preAllocations are correct", + expectErr: false, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]IPAddressStr{ + "alloc": IPAddressStr("192.168.0.2"), + }, + }, + oldPoolSpec: &IPPoolSpec{ NamePrefix: "abcd", }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]IPAddressStr{ + "inuse": IPAddressStr("192.168.0.3"), + }, + }, + }, + { + name: "should fail when preAllocations are incorrect", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]IPAddressStr{ + "alloc": IPAddressStr("192.168.0.20"), + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]IPAddressStr{ + "inuse": IPAddressStr("192.168.0.3"), + }, + }, + }, + { + name: "should fail when ip in use", + expectErr: true, + newPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + Pools: []Pool{ + {Start: &startAddr, End: &endAddr}, + }, + PreAllocations: map[string]IPAddressStr{ + "alloc": IPAddressStr("192.168.0.2"), + }, + }, + oldPoolSpec: &IPPoolSpec{ + NamePrefix: "abcd", + }, + oldPoolStatus: IPPoolStatus{ + Allocations: map[string]IPAddressStr{ + "inuse": IPAddressStr("192.168.0.30"), + }, + }, }, } @@ -109,15 +176,16 @@ func TestIPPoolUpdateValidation(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, - Spec: *tt.newPool, + Spec: *tt.newPoolSpec, } - if tt.oldPool != nil { + if tt.oldPoolSpec != nil { oldPool = &IPPool{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", }, - Spec: *tt.oldPool, + Spec: *tt.oldPoolSpec, + Status: tt.oldPoolStatus, } } else { oldPool = nil diff --git a/api/v1alpha1/utils.go b/api/v1alpha1/utils.go new file mode 100644 index 00000000..72d82827 --- /dev/null +++ b/api/v1alpha1/utils.go @@ -0,0 +1,126 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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" + "math/big" + "net" + + "github.com/pkg/errors" +) + +// GetIPAddress renders the IP address, taking the index, offset and step into +// account, it is IP version agnostic +func GetIPAddress(entry Pool, index int) (IPAddressStr, error) { + + if entry.Start == nil && entry.Subnet == nil { + return "", errors.New("Either Start or Subnet is required for ipAddress") + } + var ip net.IP + var err error + var ipNet *net.IPNet + offset := index + + // If start is given, use it to add the offset + if entry.Start != nil { + var endIP net.IP + if entry.End != nil { + endIP = net.ParseIP(string(*entry.End)) + } + ip, err = addOffsetToIP(net.ParseIP(string(*entry.Start)), endIP, offset) + if err != nil { + return "", err + } + + // Verify that the IP is in the subnet + if entry.Subnet != nil { + _, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) + if err != nil { + return "", err + } + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + + // If it is not given, use the CIDR ip address and increment the offset by 1 + } else { + ip, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) + if err != nil { + return "", err + } + offset++ + ip, err = addOffsetToIP(ip, nil, offset) + if err != nil { + return "", err + } + + // Verify that the ip is in the subnet + if !ipNet.Contains(ip) { + return "", errors.New("IP address out of bonds") + } + } + return IPAddressStr(ip.String()), nil +} + +// addOffsetToIP computes the value of the IP address with the offset. It is +// IP version agnostic +// Note that if the resulting IP address is in the format ::ffff:xxxx:xxxx then +// ip.String will fail to select the correct type of ip +func addOffsetToIP(ip, endIP net.IP, offset int) (net.IP, error) { + ip4 := false + //ip := net.ParseIP(ipString) + if ip.To4() != nil { + ip4 = true + } + + // Create big integers + IPInt := big.NewInt(0) + OffsetInt := big.NewInt(int64(offset)) + + // Transform the ip into an int. (big endian function) + IPInt = IPInt.SetBytes(ip) + + // add the two integers + IPInt = IPInt.Add(IPInt, OffsetInt) + + // return the bytes list + IPBytes := IPInt.Bytes() + + IPBytesLen := len(IPBytes) + + // Verify that the IPv4 or IPv6 fulfills theirs constraints + if (ip4 && IPBytesLen > 6 && IPBytes[4] != 255 && IPBytes[5] != 255) || + (!ip4 && IPBytesLen > 16) { + return nil, errors.New(fmt.Sprintf("IP address overflow for : %s", ip.String())) + } + + //transform the end ip into an Int to compare + if endIP != nil { + endIPInt := big.NewInt(0) + endIPInt = endIPInt.SetBytes(endIP) + // Computed IP is higher than the end IP + if IPInt.Cmp(endIPInt) > 0 { + return nil, errors.New(fmt.Sprintf("IP address out of bonds for : %s", ip.String())) + } + } + + // COpy the output back into an ip + copy(ip[16-IPBytesLen:], IPBytes) + return ip, nil +} diff --git a/api/v1alpha1/utils_test.go b/api/v1alpha1/utils_test.go new file mode 100644 index 00000000..edcb0b08 --- /dev/null +++ b/api/v1alpha1/utils_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 ( + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "k8s.io/utils/pointer" +) + +var _ = Describe("IPPool manager", func() { + type testCaseGetIPAddress struct { + ipAddress Pool + index int + expectError bool + expectedIP IPAddressStr + } + + DescribeTable("Test getIPAddress", + func(tc testCaseGetIPAddress) { + result, err := GetIPAddress(tc.ipAddress, tc.index) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(tc.expectedIP)) + } + }, + Entry("Empty Start and Subnet", testCaseGetIPAddress{ + ipAddress: Pool{}, + index: 1, + expectError: true, + }), + Entry("Start set, no end or subnet", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + }, + index: 1, + expectedIP: IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end set, subnet unset", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*IPAddressStr)(pointer.StringPtr("192.168.0.100")), + }, + index: 1, + expectedIP: IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end set, subnet unset, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + End: (*IPAddressStr)(pointer.StringPtr("192.168.0.100")), + }, + index: 100, + expectError: true, + }), + Entry("Start set, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), + }, + index: 1, + expectedIP: IPAddressStr("192.168.0.11"), + }), + Entry("Start set, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), + }, + index: 250, + expectError: true, + }), + Entry("Start set, end unset, subnet empty", testCaseGetIPAddress{ + ipAddress: Pool{ + Start: (*IPAddressStr)(pointer.StringPtr("192.168.0.10")), + Subnet: (*IPSubnetStr)(pointer.StringPtr("")), + }, + index: 1, + expectError: true, + }), + Entry("subnet empty", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: (*IPSubnetStr)(pointer.StringPtr("")), + }, + index: 1, + expectError: true, + }), + Entry("Start unset, end unset, subnet set", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: (*IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), + }, + index: 1, + expectedIP: IPAddressStr("192.168.0.12"), + }), + Entry("Start unset, end unset, subnet set, out of bound", testCaseGetIPAddress{ + ipAddress: Pool{ + Subnet: (*IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), + }, + index: 250, + expectError: true, + }), + ) + + type testCaseAddOffsetToIP struct { + ip string + endIP string + offset int + expectedIP string + expectError bool + } + + DescribeTable("Test AddOffsetToIP", + func(tc testCaseAddOffsetToIP) { + testIP := net.ParseIP(tc.ip) + testEndIP := net.ParseIP(tc.endIP) + expectedIP := net.ParseIP(tc.expectedIP) + + result, err := addOffsetToIP(testIP, testEndIP, tc.offset) + if tc.expectError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(expectedIP)) + } + }, + Entry("valid IPv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 10, + expectedIP: "192.168.0.20", + }), + Entry("valid IPv4, no end ip", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + offset: 1000, + expectedIP: "192.168.3.242", + }), + Entry("Over bound ipv4", testCaseAddOffsetToIP{ + ip: "192.168.0.10", + endIP: "192.168.0.200", + offset: 1000, + expectError: true, + }), + Entry("error ipv4", testCaseAddOffsetToIP{ + ip: "255.255.255.250", + offset: 10, + expectError: true, + }), + Entry("valid IPv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::fff0", + offset: 10, + expectedIP: "2001::1A", + }), + Entry("valid IPv6, no end ip", testCaseAddOffsetToIP{ + ip: "2001::10", + offset: 10000, + expectedIP: "2001::2720", + }), + Entry("Over bound ipv6", testCaseAddOffsetToIP{ + ip: "2001::10", + endIP: "2001::00f0", + offset: 10000, + expectError: true, + }), + Entry("error ipv6", testCaseAddOffsetToIP{ + ip: "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFF0", + offset: 100, + expectError: true, + }), + ) + +}) diff --git a/ipam/ippool_manager.go b/ipam/ippool_manager.go index 421b7f1a..2b117322 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -18,9 +18,6 @@ package ipam import ( "context" - "fmt" - "math/big" - "net" "reflect" "strings" @@ -240,40 +237,64 @@ func (m *IPPoolManager) updateAddress(ctx context.Context, func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, addresses map[ipamv1.IPAddressStr]string, ) (ipamv1.IPAddressStr, int, *ipamv1.IPAddressStr, []ipamv1.IPAddressStr, error) { + var allocatedAddress ipamv1.IPAddressStr = "" var err error // Get pre-allocated addresses - allocatedAddress, ipAllocated := m.IPPool.Spec.PreAllocations[addressClaim.Name] + preAllocatedAddress, ipPreAllocated := m.IPPool.Spec.PreAllocations[addressClaim.Name] // If the IP is pre-allocated, the default prefix and gateway are used prefix := m.IPPool.Spec.Prefix gateway := m.IPPool.Spec.Gateway dnsServers := m.IPPool.Spec.DNSServers + ipAllocated := false + for _, pool := range m.IPPool.Spec.Pools { if ipAllocated { break } index := 0 for !ipAllocated { - allocatedAddress, err = getIPAddress(pool, index) + allocatedAddress, err = ipamv1.GetIPAddress(pool, index) if err != nil { break } index++ + // We have a pre-allocated ip, we just need to ensure that it matches the current address + // if it does not, continue and try the next address + if ipPreAllocated && allocatedAddress != preAllocatedAddress { + continue + } + // Here the two addresses match, so we continue with that one + if ipPreAllocated { + ipAllocated = true + } + // If we have a preallocated address, this is useless, otherwise, check if the + // ip is free if _, ok := addresses[allocatedAddress]; !ok && allocatedAddress != "" { ipAllocated = true - if pool.Prefix != 0 { - prefix = pool.Prefix - } - if pool.Gateway != nil { - gateway = pool.Gateway - } - if len(pool.DNSServers) != 0 { - dnsServers = pool.DNSServers - } + } + if !ipAllocated { + continue + } + + if pool.Prefix != 0 { + prefix = pool.Prefix + } + if pool.Gateway != nil { + gateway = pool.Gateway + } + if len(pool.DNSServers) != 0 { + dnsServers = pool.DNSServers } } } + // We have a preallocated IP but we did not find it in the pools! It means it is + // misconfigured + if !ipAllocated && ipPreAllocated { + addressClaim.Status.ErrorMessage = pointer.StringPtr("Pre-allocated IP out of bond") + return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("Pre-allocated IP out of bond") + } if !ipAllocated { addressClaim.Status.ErrorMessage = pointer.StringPtr("Exhausted IP Pools") return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("Exhausted IP Pools") @@ -423,107 +444,6 @@ func (m *IPPoolManager) deleteAddress(ctx context.Context, return addresses, nil } -// getIPAddress renders the IP address, taking the index, offset and step into -// account, it is IP version agnostic -func getIPAddress(entry ipamv1.Pool, index int) (ipamv1.IPAddressStr, error) { - - if entry.Start == nil && entry.Subnet == nil { - return "", errors.New("Either Start or Subnet is required for ipAddress") - } - var ip net.IP - var err error - var ipNet *net.IPNet - offset := index - - // If start is given, use it to add the offset - if entry.Start != nil { - var endIP net.IP - if entry.End != nil { - endIP = net.ParseIP(string(*entry.End)) - } - ip, err = addOffsetToIP(net.ParseIP(string(*entry.Start)), endIP, offset) - if err != nil { - return "", err - } - - // Verify that the IP is in the subnet - if entry.Subnet != nil { - _, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) - if err != nil { - return "", err - } - if !ipNet.Contains(ip) { - return "", errors.New("IP address out of bonds") - } - } - - // If it is not given, use the CIDR ip address and increment the offset by 1 - } else { - ip, ipNet, err = net.ParseCIDR(string(*entry.Subnet)) - if err != nil { - return "", err - } - offset++ - ip, err = addOffsetToIP(ip, nil, offset) - if err != nil { - return "", err - } - - // Verify that the ip is in the subnet - if !ipNet.Contains(ip) { - return "", errors.New("IP address out of bonds") - } - } - return ipamv1.IPAddressStr(ip.String()), nil -} - -// addOffsetToIP computes the value of the IP address with the offset. It is -// IP version agnostic -// Note that if the resulting IP address is in the format ::ffff:xxxx:xxxx then -// ip.String will fail to select the correct type of ip -func addOffsetToIP(ip, endIP net.IP, offset int) (net.IP, error) { - ip4 := false - //ip := net.ParseIP(ipString) - if ip.To4() != nil { - ip4 = true - } - - // Create big integers - IPInt := big.NewInt(0) - OffsetInt := big.NewInt(int64(offset)) - - // Transform the ip into an int. (big endian function) - IPInt = IPInt.SetBytes(ip) - - // add the two integers - IPInt = IPInt.Add(IPInt, OffsetInt) - - // return the bytes list - IPBytes := IPInt.Bytes() - - IPBytesLen := len(IPBytes) - - // Verify that the IPv4 or IPv6 fulfills theirs constraints - if (ip4 && IPBytesLen > 6 && IPBytes[4] != 255 && IPBytes[5] != 255) || - (!ip4 && IPBytesLen > 16) { - return nil, errors.New(fmt.Sprintf("IP address overflow for : %s", ip.String())) - } - - //transform the end ip into an Int to compare - if endIP != nil { - endIPInt := big.NewInt(0) - endIPInt = endIPInt.SetBytes(endIP) - // Computed IP is higher than the end IP - if IPInt.Cmp(endIPInt) > 0 { - return nil, errors.New(fmt.Sprintf("IP address out of bonds for : %s", ip.String())) - } - } - - // COpy the output back into an ip - copy(ip[16-IPBytesLen:], IPBytes) - return ip, nil -} - // formatAddressName renders the name of the IPAddress objects func (m *IPPoolManager) formatAddressName(address ipamv1.IPAddressStr) string { return strings.TrimRight(m.IPPool.Spec.NamePrefix+"-"+strings.Replace( diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 0e1d778d..e915ebc6 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -19,7 +19,6 @@ package ipam import ( "context" "fmt" - "net" "reflect" . "github.com/onsi/ginkgo" @@ -744,9 +743,13 @@ var _ = Describe("IPPool manager", func() { ipamv1.IPAddressStr("8.8.8.8"), }, }, + ipamv1.Pool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.21")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.30")), + }, }, PreAllocations: map[string]ipamv1.IPAddressStr{ - "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + "TestRef": ipamv1.IPAddressStr("192.168.0.21"), }, Prefix: 24, Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), @@ -760,13 +763,80 @@ var _ = Describe("IPPool manager", func() { Name: "TestRef", }, }, - expectedAddress: ipamv1.IPAddressStr("192.168.0.15"), + expectedAddress: ipamv1.IPAddressStr("192.168.0.21"), expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), expectedDNSServers: []ipamv1.IPAddressStr{ ipamv1.IPAddressStr("8.8.4.4"), }, expectedPrefix: 24, }), + Entry("One pool, pre-allocated, with overrides", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.15"), + expectedGateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + expectedDNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + expectedPrefix: 26, + }), + Entry("One pool, pre-allocated, out of bonds", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + ipamv1.Pool{ + Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.20")), + Prefix: 26, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.1.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.8.8"), + }, + }, + }, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.21"), + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.1")), + DNSServers: []ipamv1.IPAddressStr{ + ipamv1.IPAddressStr("8.8.4.4"), + }, + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + }, + }, + expectError: true, + }), Entry("One pool, with start and existing address", testCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ @@ -1074,164 +1144,4 @@ var _ = Describe("IPPool manager", func() { }), ) - type testCaseGetIPAddress struct { - ipAddress ipamv1.Pool - index int - expectError bool - expectedIP ipamv1.IPAddressStr - } - - DescribeTable("Test getIPAddress", - func(tc testCaseGetIPAddress) { - result, err := getIPAddress(tc.ipAddress, tc.index) - if tc.expectError { - Expect(err).To(HaveOccurred()) - } else { - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(tc.expectedIP)) - } - }, - Entry("Empty Start and Subnet", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{}, - index: 1, - expectError: true, - }), - Entry("Start set, no end or subnet", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - }, - index: 1, - expectedIP: ipamv1.IPAddressStr("192.168.0.11"), - }), - Entry("Start set, end set, subnet unset", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), - }, - index: 1, - expectedIP: ipamv1.IPAddressStr("192.168.0.11"), - }), - Entry("Start set, end set, subnet unset, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - End: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.100")), - }, - index: 100, - expectError: true, - }), - Entry("Start set, end unset, subnet set", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), - }, - index: 1, - expectedIP: ipamv1.IPAddressStr("192.168.0.11"), - }), - Entry("Start set, end unset, subnet set, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.0/24")), - }, - index: 250, - expectError: true, - }), - Entry("Start set, end unset, subnet empty", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Start: (*ipamv1.IPAddressStr)(pointer.StringPtr("192.168.0.10")), - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), - }, - index: 1, - expectError: true, - }), - Entry("subnet empty", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("")), - }, - index: 1, - expectError: true, - }), - Entry("Start unset, end unset, subnet set", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), - }, - index: 1, - expectedIP: ipamv1.IPAddressStr("192.168.0.12"), - }), - Entry("Start unset, end unset, subnet set, out of bound", testCaseGetIPAddress{ - ipAddress: ipamv1.Pool{ - Subnet: (*ipamv1.IPSubnetStr)(pointer.StringPtr("192.168.0.10/24")), - }, - index: 250, - expectError: true, - }), - ) - - type testCaseAddOffsetToIP struct { - ip string - endIP string - offset int - expectedIP string - expectError bool - } - - DescribeTable("Test AddOffsetToIP", - func(tc testCaseAddOffsetToIP) { - testIP := net.ParseIP(tc.ip) - testEndIP := net.ParseIP(tc.endIP) - expectedIP := net.ParseIP(tc.expectedIP) - - result, err := addOffsetToIP(testIP, testEndIP, tc.offset) - if tc.expectError { - Expect(err).To(HaveOccurred()) - } else { - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(expectedIP)) - } - }, - Entry("valid IPv4", testCaseAddOffsetToIP{ - ip: "192.168.0.10", - endIP: "192.168.0.200", - offset: 10, - expectedIP: "192.168.0.20", - }), - Entry("valid IPv4, no end ip", testCaseAddOffsetToIP{ - ip: "192.168.0.10", - offset: 1000, - expectedIP: "192.168.3.242", - }), - Entry("Over bound ipv4", testCaseAddOffsetToIP{ - ip: "192.168.0.10", - endIP: "192.168.0.200", - offset: 1000, - expectError: true, - }), - Entry("error ipv4", testCaseAddOffsetToIP{ - ip: "255.255.255.250", - offset: 10, - expectError: true, - }), - Entry("valid IPv6", testCaseAddOffsetToIP{ - ip: "2001::10", - endIP: "2001::fff0", - offset: 10, - expectedIP: "2001::1A", - }), - Entry("valid IPv6, no end ip", testCaseAddOffsetToIP{ - ip: "2001::10", - offset: 10000, - expectedIP: "2001::2720", - }), - Entry("Over bound ipv6", testCaseAddOffsetToIP{ - ip: "2001::10", - endIP: "2001::00f0", - offset: 10000, - expectError: true, - }), - Entry("error ipv6", testCaseAddOffsetToIP{ - ip: "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFF0", - offset: 100, - expectError: true, - }), - ) - })