diff --git a/.gitignore b/.gitignore index c2ef215769..d7a77cd042 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,7 @@ token /liqoctl # test generated files -coverage.txt +coverage* # example kubeconfig generates by examples liqo_kubeconf* diff --git a/build/liqo-test/Dockerfile b/build/liqo-test/Dockerfile index f8a1a01bec..461df16383 100644 --- a/build/liqo-test/Dockerfile +++ b/build/liqo-test/Dockerfile @@ -22,5 +22,6 @@ ENTRYPOINT [ "go", "test" ] # Remove the -count=1 flag to enable caching of successful tests # Remove the -failfast flag to run all tests even if some fail +# Remove the -shuffle flag to run tests in the order they are defined # Add the -v flag to increase verbosity -CMD [ "./cmd/...", "./pkg/...", "./apis/...", "./internal/...", "-count=1", "-failfast" ] +CMD [ "./cmd/...", "./pkg/...", "./apis/...", "./internal/...", "-count=1", "-failfast", "-shuffle=on" ] diff --git a/pkg/ipam/core/core_suite_test.go b/pkg/ipam/core/core_suite_test.go new file mode 100644 index 0000000000..75ba3b46d0 --- /dev/null +++ b/pkg/ipam/core/core_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2019-2024 The Liqo 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 ipamcore_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Core Suite") +} diff --git a/pkg/ipam/core/ipam.go b/pkg/ipam/core/ipam.go index e2adb94203..9389a61c94 100644 --- a/pkg/ipam/core/ipam.go +++ b/pkg/ipam/core/ipam.go @@ -26,19 +26,14 @@ type Ipam struct { } // NewIpam creates a new IPAM instance. -func NewIpam(pools []string) (*Ipam, error) { - ipamRootsPrefixes := make([]netip.Prefix, len(pools)) - for i, root := range pools { - ipamRootsPrefixes[i] = netip.MustParsePrefix(root) - } - - if err := checkRoots(ipamRootsPrefixes); err != nil { +func NewIpam(pools []netip.Prefix) (*Ipam, error) { + if err := checkRoots(pools); err != nil { return nil, err } ipamRoots := make([]node, len(pools)) - for i := range ipamRootsPrefixes { - ipamRoots[i] = newNode(ipamRootsPrefixes[i]) + for i := range pools { + ipamRoots[i] = newNode(pools[i]) } ipam := &Ipam{ @@ -252,5 +247,5 @@ func (ipam *Ipam) IPSetCreationTimestamp(addr netip.Addr, prefix netip.Prefix, c return nil } } - return nil + return fmt.Errorf("IP address %s not found", addr) } diff --git a/pkg/ipam/core/ipam_test.go b/pkg/ipam/core/ipam_test.go new file mode 100644 index 0000000000..064069d2a0 --- /dev/null +++ b/pkg/ipam/core/ipam_test.go @@ -0,0 +1,718 @@ +// Copyright 2019-2024 The Liqo 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 ipamcore + +import ( + "fmt" + "math" + "net/netip" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ipam", func() { + var ( + err error + ipam *Ipam + validPools = []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("192.168.0.0/16"), + netip.MustParsePrefix("172.16.0.0/12"), + } + invalidPools = []netip.Prefix{ + netip.MustParsePrefix("10.0.1.0/8"), + netip.MustParsePrefix("192.168.1.0/16"), + netip.MustParsePrefix("172.16.0.5/12"), + } + prefixOutOfPools = netip.MustParsePrefix("11.0.0.0/8") + ) + + BeforeEach(func() { + var err error + ipam, err = NewIpam(validPools) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("Ipam creation", func() { + When("Using valid pools", func() { + It("should create an Ipam object", func() { + ipam, err = NewIpam(validPools) + Expect(err).NotTo(HaveOccurred()) + Expect(ipam).NotTo(BeNil()) + }) + }) + + When("Using invalid pools", func() { + It("should return an error", func() { + _, err := NewIpam(invalidPools) + Expect(err).To(HaveOccurred()) + }) + + It("Should return false", func() { + poolsFalse := []netip.Prefix{ + netip.MustParsePrefix("1.0.0.0/8"), + netip.MustParsePrefix("0.0.0.0/0"), + } + poolsTrue := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/24"), + netip.MustParsePrefix("192.168.0.0/30"), + } + for _, pool := range poolsFalse { + Expect(ipam.IsPrefixInRoots(pool)).To(BeFalse()) + } + for _, pool := range poolsTrue { + Expect(ipam.IsPrefixInRoots(pool)).To(BeTrue()) + } + }) + }) + }) + + Context("Ipam utilities", func() { + When("checking if a prefix is child of another one", func() { + It("should return true", func() { + parentPrefix := netip.MustParsePrefix("10.0.0.0/16") + childPrefixes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/24"), + netip.MustParsePrefix("10.0.1.0/24"), + netip.MustParsePrefix("10.0.1.6/24"), + netip.MustParsePrefix("10.0.0.0/16"), + } + + for _, childPrefix := range childPrefixes { + Expect(isPrefixChildOf(parentPrefix, childPrefix)).To(BeTrue()) + } + }) + + It("should return false", func() { + parentPrefix := netip.MustParsePrefix("10.0.0.0/16") + childPrefixes := []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("10.0.0.0/15"), + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("10.0.1.0/8"), + } + + for _, childPrefix := range childPrefixes { + Expect(isPrefixChildOf(parentPrefix, childPrefix)).To(BeFalse()) + } + }) + }) + + When("forcing a network last update timestamp", func() { + var ( + acquiredPrefix *netip.Prefix + ) + + BeforeEach(func() { + acquiredPrefix = ipam.NetworkAcquire(32) + Expect(acquiredPrefix).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(*acquiredPrefix)).To(BeFalse()) + }) + + It("should succeed", func() { + newTime := time.Now().Add(time.Hour) + Expect(ipam.NetworkSetLastUpdateTimestamp(*acquiredPrefix, newTime)).Should(Succeed()) + node := search(*acquiredPrefix, &ipam.roots[0]) + Expect(node).NotTo(BeNil()) + Expect(node.lastUpdateTimestamp).To(Equal(newTime)) + }) + + It("should not succeed", func() { + Expect(ipam.NetworkSetLastUpdateTimestamp(prefixOutOfPools, time.Now())).ShouldNot(Succeed()) + Expect(ipam.NetworkRelease(*acquiredPrefix, 0).String()).To(Equal(acquiredPrefix.String())) + Expect(ipam.NetworkIsAvailable(*acquiredPrefix)).To(BeTrue()) + Expect(ipam.NetworkSetLastUpdateTimestamp(*acquiredPrefix, time.Now())).ShouldNot(Succeed()) + }) + }) + + When("forcing an IP creation timestamp", func() { + var ( + acquiredPrefix *netip.Prefix + acquiredAddr *netip.Addr + ) + + BeforeEach(func() { + acquiredPrefix = ipam.NetworkAcquire(32) + Expect(acquiredPrefix).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(*acquiredPrefix)).To(BeFalse()) + + acquiredAddr, err = ipam.IPAcquire(*acquiredPrefix) + Expect(err).NotTo(HaveOccurred()) + Expect(acquiredAddr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(*acquiredPrefix, *acquiredAddr)).To(BeTrue()) + }) + + It("should succeed", func() { + newTime := time.Now().Add(time.Hour) + Expect(ipam.IPSetCreationTimestamp(*acquiredAddr, *acquiredPrefix, newTime)).Should(Succeed()) + node := search(*acquiredPrefix, &ipam.roots[0]) + Expect(node).NotTo(BeNil()) + Expect(node.ips).Should(HaveLen(1)) + Expect(node.ips[0].creationTimestamp).To(Equal(newTime)) + }) + + It("should not succeed", func() { + Expect(ipam.IPSetCreationTimestamp(*acquiredAddr, prefixOutOfPools, time.Now())).ShouldNot(Succeed()) + + releasedIP, err := ipam.IPRelease(*acquiredPrefix, *acquiredAddr, 0) + Expect(err).NotTo(HaveOccurred()) + Expect(releasedIP).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(*acquiredPrefix, *acquiredAddr)).To(BeFalse()) + + Expect(ipam.IPSetCreationTimestamp(*acquiredAddr, *acquiredPrefix, time.Now())).ShouldNot(Succeed()) + + Expect(ipam.NetworkRelease(*acquiredPrefix, 0).String()).To(Equal(acquiredPrefix.String())) + Expect(ipam.NetworkIsAvailable(*acquiredPrefix)).To(BeTrue()) + + Expect(ipam.IPSetCreationTimestamp(*acquiredAddr, *acquiredPrefix, time.Now())).ShouldNot(Succeed()) + }) + }) + + When("generating graphviz", func() { + BeforeEach(func() { + sizes := []int{21, 26, 27, 22, 30, 25, 28, 24, 16, 10, 29} + for _, size := range sizes { + for i := 0; i < 3; i++ { + network := ipam.NetworkAcquire(size) + Expect(network).ShouldNot(BeNil()) + for j := 0; j < 3; j++ { + addr, err := ipam.IPAcquire(*network) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + } + } + } + }) + + It("it should succeed", func() { + Expect(ipam.ToGraphviz()).Should(Succeed()) + }) + + AfterEach(func() { + _, err := os.Stat(graphvizFolder) + Expect(err).ShouldNot(HaveOccurred()) + Expect(os.RemoveAll(graphvizFolder)).ShouldNot(HaveOccurred()) + }) + }) + }) + + Context("Ipam networks", func() { + BeforeEach(func() { + var err error + ipam, err = NewIpam(validPools) + Expect(err).NotTo(HaveOccurred()) + }) + + When("creating an Ipam object", func() { + It("should succeed", func() { + Expect(ipam).NotTo(BeNil()) + }) + }) + + When("listing networks", func() { + It("should succeed", func() { + networks := ipam.ListNetworks() + Expect(networks).Should(HaveLen(0)) + + acquiredNetworks := []netip.Prefix{} + acquiredNetworks = append(acquiredNetworks, *ipam.NetworkAcquire(24)) + acquiredNetworks = append(acquiredNetworks, *ipam.NetworkAcquire(25)) + acquiredNetworks = append(acquiredNetworks, *ipam.NetworkAcquire(26)) + acquiredNetworks = append(acquiredNetworks, *ipam.NetworkAcquire(27)) + acquiredNetworks = append(acquiredNetworks, *ipam.NetworkAcquire(28)) + + networks = ipam.ListNetworks() + Expect(networks).Should(HaveLen(5)) + for i := range acquiredNetworks { + Expect(networks).Should(ContainElement(acquiredNetworks[i])) + } + }) + }) + + When("acquiring networks", func() { + It("should succeed", func() { + network := ipam.NetworkAcquire(24) + Expect(network).ShouldNot(BeNil()) + Expect(ipam.NetworkIsAvailable(*network)).To(BeFalse()) + }) + + It("should not succeed", func() { + network := ipam.NetworkAcquire(4) + Expect(network).Should(BeNil()) + }) + }) + + When("releasing networks", func() { + It("should succeed", func() { + network := ipam.NetworkAcquire(16) + Expect(network).ShouldNot(BeNil()) + Expect(ipam.NetworkIsAvailable(*network)).To(BeFalse()) + Expect(ipam.NetworkRelease(*network, 0).String()).To(Equal(network.String())) + Expect(ipam.NetworkIsAvailable(*network)).To(BeTrue()) + }) + + It("should succeed (using root prefix)", func() { + network := validPools[0] + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + Expect(ipam.NetworkAcquireWithPrefix(network).String()).To(Equal(network.String())) + Expect(ipam.NetworkIsAvailable(network)).To(BeFalse()) + Expect(ipam.NetworkRelease(network, 0).String()).To(Equal(network.String())) + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + }) + + It("should succeed (with grace period expired)", func() { + gracePeriod := time.Second * 5 + network := ipam.NetworkAcquire(16) + Expect(ipam.NetworkSetLastUpdateTimestamp(*network, time.Now().Add(-gracePeriod))).Should(Succeed()) + Expect(network).ShouldNot(BeNil()) + Expect(ipam.NetworkIsAvailable(*network)).To(BeFalse()) + + Expect(ipam.NetworkRelease(*network, gracePeriod).String()).To(Equal(network.String())) + Expect(ipam.NetworkIsAvailable(*network)).To(BeTrue()) + }) + + It("should not succeed", func() { + networks := []netip.Prefix{ + netip.MustParsePrefix("10.0.1.0/24"), + netip.MustParsePrefix("10.0.2.0/24"), + netip.MustParsePrefix("10.1.0.0/16"), + netip.MustParsePrefix("10.2.0.0/16"), + netip.MustParsePrefix("10.3.0.0/30"), + netip.MustParsePrefix("10.4.0.0/27"), + } + + for _, network := range networks { + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + Expect(ipam.NetworkRelease(network, 0)).To(BeNil()) + Expect(ipam.NetworkRelease(network, time.Second*5)).To(BeNil()) + } + }) + + It("should not succeed (with allocated subnet)", func() { + subnetwork := netip.MustParsePrefix("10.0.0.0/30") + Expect(ipam.NetworkIsAvailable(subnetwork)).To(BeTrue()) + Expect(ipam.NetworkAcquireWithPrefix(subnetwork)).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(subnetwork)).To(BeFalse()) + + network := netip.MustParsePrefix("10.0.0.0/24") + Expect(ipam.NetworkIsAvailable(network)).To(BeFalse()) + Expect(ipam.NetworkRelease(network, 0)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(network)).To(BeFalse()) + }) + + It("should not succeed (with root prefix)", func() { + network := validPools[0] + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + Expect(ipam.NetworkRelease(network, 0)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + }) + }) + + When("acquiring networks with prefix", func() { + It("should succeed", func() { + networks := []netip.Prefix{ + netip.MustParsePrefix("10.0.1.0/24"), + netip.MustParsePrefix("10.0.2.0/24"), + netip.MustParsePrefix("10.1.0.0/16"), + netip.MustParsePrefix("10.2.0.0/16"), + netip.MustParsePrefix("10.3.8.0/21"), + netip.MustParsePrefix("10.4.16.0/20"), + netip.MustParsePrefix("10.128.0.0/20"), + netip.MustParsePrefix("10.130.64.0/18"), + netip.MustParsePrefix("10.4.2.0/27"), + netip.MustParsePrefix("10.3.0.0/30"), + netip.MustParsePrefix("10.4.0.0/27"), + netip.MustParsePrefix("10.5.0.0/16"), + netip.MustParsePrefix("10.4.2.128/25"), + netip.MustParsePrefix("10.4.3.0/27"), + netip.MustParsePrefix("10.3.0.24/29"), + } + + for _, network := range networks { + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + } + + for _, network := range networks { + networkAcquired := ipam.NetworkAcquireWithPrefix(network) + Expect(networkAcquired).NotTo(BeNil()) + Expect(networkAcquired.String()).To(Equal(network.String())) + } + + for _, network := range networks { + Expect(ipam.NetworkIsAvailable(network)).To(BeFalse()) + } + + for _, network := range networks { + Expect(ipam.NetworkRelease(network, 0).String()).To(Equal(network.String())) + } + + for _, network := range networks { + Expect(ipam.NetworkIsAvailable(network)).To(BeTrue()) + } + }) + + It("should not succeed", func() { + networks := []netip.Prefix{ + netip.MustParsePrefix("10.0.1.0/8"), + netip.MustParsePrefix("11.0.2.0/24"), + netip.MustParsePrefix("11.1.0.0/16"), + netip.MustParsePrefix("11.2.0.0/16"), + netip.MustParsePrefix("12.0.0.0/8"), + netip.MustParsePrefix("13.4.0.0/8"), + } + + for _, network := range networks { + Expect(ipam.NetworkAcquireWithPrefix(network)).To(BeNil()) + } + }) + }) + + When("acquiring a network", func() { + BeforeEach(func() { + prefix := netip.MustParsePrefix("10.0.0.0/16") + Expect(ipam.NetworkAcquireWithPrefix(prefix)).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.5.0.0/16") + Expect(ipam.NetworkAcquireWithPrefix(prefix)).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + }) + + It("parent networks should not be available", func() { + prefix := netip.MustParsePrefix("10.0.0.0/14") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.0.0.0/15") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.4.0.0/15") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.4.0.0/14") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.0.0.0/13") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + }) + + It("child networks should not be available", func() { + prefix := netip.MustParsePrefix("10.0.0.0/17") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.0.0.0/18") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("10.5.0.0/20") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("11.0.0.0/16") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + + prefix = netip.MustParsePrefix("0.0.0.0/0") + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + Expect(ipam.NetworkAcquireWithPrefix(prefix)).To(BeNil()) + Expect(ipam.NetworkIsAvailable(prefix)).To(BeFalse()) + }) + }) + }) + + Context("Ipam IPs", func() { + var ( + // WARNING: availableIPs must be a power of 2 + availableIPs = 256 + prefixAcquired = netip.MustParsePrefix(fmt.Sprintf("10.0.0.0/%d", int(32-math.Log2(float64(availableIPs))))) + prefixNotAcquired = netip.MustParsePrefix(fmt.Sprintf("10.1.0.0/%d", int(32-math.Log2(float64(availableIPs))))) + subPrefixNotAcquired = netip.MustParsePrefix(fmt.Sprintf("10.1.0.0/%d", int(32-math.Log2(float64(availableIPs)))+1)) + ) + + BeforeEach(func() { + var err error + ipam, err = NewIpam(validPools) + Expect(err).NotTo(HaveOccurred()) + + Expect(ipam.NetworkIsAvailable(prefixAcquired)).To(BeTrue()) + Expect(ipam.NetworkAcquireWithPrefix(prefixAcquired)).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(prefixAcquired)).To(BeFalse()) + }) + + When("acquiring an IP from not existing network", func() { + It("should not succeed (out of pools)", func() { + addr, err := ipam.IPAcquire(prefixOutOfPools) + Expect(err).To(HaveOccurred()) + Expect(addr).To(BeNil()) + + allocated, err := ipam.IPIsAllocated(prefixOutOfPools, prefixOutOfPools.Addr()) + Expect(err).To(HaveOccurred()) + Expect(allocated).To(BeFalse()) + + addr, err = ipam.IPAcquireWithAddr(prefixOutOfPools, prefixOutOfPools.Addr()) + Expect(err).To(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + + It("should not succeed (prefix not acquired)", func() { + addr, err := ipam.IPAcquire(prefixNotAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + + addr, err = ipam.IPAcquireWithAddr(prefixNotAcquired, prefixNotAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + + It("should not succeed (prefix not acquired with subprefix acquired)", func() { + Expect(ipam.NetworkIsAvailable(subPrefixNotAcquired)).To(BeTrue()) + Expect(ipam.NetworkAcquireWithPrefix(subPrefixNotAcquired)).NotTo(BeNil()) + Expect(ipam.NetworkIsAvailable(subPrefixNotAcquired)).To(BeFalse()) + + addr, err := ipam.IPAcquire(prefixNotAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + + addr, err = ipam.IPAcquireWithAddr(prefixNotAcquired, prefixNotAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + }) + + When("acquiring an IP from existing network", func() { + It("should succeed", func() { + allocated, err := ipam.IPIsAllocated(prefixAcquired, prefixAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(allocated).To(BeFalse()) + + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + }) + + It("should succeed, with specific IP", func() { + addr, err := ipam.IPAcquireWithAddr(prefixAcquired, prefixAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(addr.String()).To(Equal(prefixAcquired.Addr().String())) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + }) + + It("should not reallocate the same IP", func() { + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + + addr, err = ipam.IPAcquireWithAddr(prefixAcquired, *addr) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + + It("should not allocate an IP from a not coherent prefix", func() { + addr, err := ipam.IPAcquireWithAddr(prefixAcquired, prefixNotAcquired.Addr()) + Expect(err).To(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + + It("should not overflow available IPs)", func() { + for i := 0; i < availableIPs; i++ { + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + } + for i := 0; i < availableIPs; i++ { + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).To(BeNil()) + } + }) + + It("should not overflow available IPs, with specific IPs)", func() { + addr := prefixAcquired.Addr() + for i := 0; i < availableIPs; i++ { + result, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *result)).To(BeTrue()) + addr = addr.Next() + } + for i := 0; i < availableIPs; i++ { + result, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeNil()) + addr = addr.Next() + } + }) + + It("should succeed (after an ip has been released)", func() { + addrs := []netip.Addr{} + for i := 0; i < availableIPs; i++ { + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + addrs = append(addrs, *addr) + } + + addr, err := ipam.IPRelease(prefixAcquired, addrs[availableIPs/2], 0) + Expect(err).To(BeNil()) + Expect(addr).NotTo(BeNil()) + Expect(addr.String()).To(Equal(addrs[availableIPs/2].String())) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeFalse()) + + addr, err = ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + }) + + It("should not succeed (prefix not coherent with addr)", func() { + addr, err := ipam.IPAcquireWithAddr(prefixAcquired, prefixNotAcquired.Addr()) + Expect(err).To(HaveOccurred()) + Expect(addr).To(BeNil()) + }) + }) + When("releasing an IP from not existing network", func() { + It("should not succeed", func() { + addr := prefixAcquired.Addr() + for i := 0; i < availableIPs*2; i++ { + result, err := ipam.IPRelease(prefixNotAcquired, addr, 0) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeNil()) + addr = addr.Next() + } + }) + + It("should not succeed (out of pools)", func() { + addr := prefixAcquired.Addr() + for i := 0; i < availableIPs*2; i++ { + result, err := ipam.IPRelease(prefixOutOfPools, addr, 0) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + addr = addr.Next() + } + }) + }) + When("releasing an IP from existing network", func() { + It("should not succeed", func() { + addr := prefixAcquired.Addr() + for i := 0; i < availableIPs*2; i++ { + result, err := ipam.IPRelease(prefixAcquired, addr, 0) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeNil()) + addr = addr.Next() + } + }) + + It("should succeed", func() { + addrs := []netip.Addr{} + for i := 0; i < availableIPs; i++ { + addr, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + addrs = append(addrs, *addr) + } + for i := range addrs { + addr, err := ipam.IPRelease(prefixAcquired, addrs[i], 0) + Expect(err).To(BeNil()) + Expect(addr).NotTo(BeNil()) + Expect(addr.String()).To(Equal(addrs[i].String())) + } + }) + + It("should not succeed (grace period not expired)", func() { + gracePeriod := time.Second * 5 + + Expect(ipam.IPIsAllocated(prefixAcquired, prefixAcquired.Addr())).To(BeFalse()) + addr, err := ipam.IPAcquireWithAddr(prefixAcquired, prefixAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(addr).NotTo(BeNil()) + Expect(ipam.IPIsAllocated(prefixAcquired, *addr)).To(BeTrue()) + + addr, err = ipam.IPRelease(prefixAcquired, *addr, gracePeriod) + Expect(err).To(BeNil()) + Expect(addr).To(BeNil()) + }) + }) + + When("listing IPs in a network", func() { + var acquiredIPs []netip.Addr + BeforeEach(func() { + addr := prefixAcquired.Addr() + for i := 0; i < availableIPs/2; i++ { + result, err := ipam.IPAcquireWithAddr(prefixAcquired, addr) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + acquiredIPs = append(acquiredIPs, addr) + addr = addr.Next() + } + for i := 0; i < availableIPs/2; i++ { + result, err := ipam.IPAcquire(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + acquiredIPs = append(acquiredIPs, *result) + } + + }) + + It("should contains the correct IPs", func() { + cachedIPs, err := ipam.ListIPs(prefixAcquired) + Expect(err).NotTo(HaveOccurred()) + for i := range acquiredIPs { + Expect(cachedIPs).Should(ContainElement(acquiredIPs[i])) + } + }) + + It("should be void", func() { + cachedIPs, err := ipam.ListIPs(prefixNotAcquired) + Expect(err).NotTo(HaveOccurred()) + Expect(cachedIPs).Should(HaveLen(0)) + }) + + It("should fail", func() { + cachedIPs, err := ipam.ListIPs(prefixOutOfPools) + Expect(err).To(HaveOccurred()) + Expect(cachedIPs).Should(HaveLen(0)) + }) + }) + + When("checking if an IP is allocated in a not allocated network", func() { + It("should return false", func() { + allocated, err := ipam.IPIsAllocated(prefixNotAcquired, prefixNotAcquired.Addr()) + Expect(err).NotTo(HaveOccurred()) + Expect(allocated).To(BeFalse()) + }) + }) + }) +}) diff --git a/pkg/ipam/core/net.go b/pkg/ipam/core/net.go index 2667f19257..f1d0f16559 100644 --- a/pkg/ipam/core/net.go +++ b/pkg/ipam/core/net.go @@ -18,6 +18,8 @@ import ( "fmt" "net/netip" "strings" + + "k8s.io/apimachinery/pkg/util/runtime" ) // convertByteSliceToString converts a slice of bytes to a comma-separated string. @@ -30,12 +32,11 @@ func convertByteSliceToString(byteSlice []byte) string { } // setBit sets the bit at the given position to 1. -func setBit(b, position byte) byte { - if position > 7 { - fmt.Println("Bit position out of range") - return b +func setBit(b byte, position int) (byte, error) { + if position > 7 || position < 0 { + return b, fmt.Errorf("bit position out of range") } - return b | (1 << (7 - position)) + return b | (1 << (7 - position)), nil } func checkHostBitsZero(prefix netip.Prefix) error { @@ -50,15 +51,11 @@ func checkHostBitsZero(prefix netip.Prefix) error { // the new position to 0 or 1 to retrieve the two subnets. func splitNetworkPrefix(prefix netip.Prefix) (left, right netip.Prefix) { // We neer to check that the host bits are zero. - if err := checkHostBitsZero(prefix); err != nil { - panic("Host bits must be zero") - } + runtime.Must(checkHostBitsZero(prefix)) // We need to convert the prefix to a byte slice to manipulate it. bin, err := prefix.MarshalBinary() - if err != nil { - panic(err) - } + runtime.Must(err) // We need to get the mask length to know where to split the prefix. maskLen := bin[len(bin)-1] @@ -75,7 +72,8 @@ func splitNetworkPrefix(prefix netip.Prefix) (left, right netip.Prefix) { bitIndex := maskLen % 8 // We set the bit at the mask length position to 1. - bin[byteIndex] = setBit(bin[byteIndex], bitIndex) + bin[byteIndex], err = setBit(bin[byteIndex], int(bitIndex)) + runtime.Must(err) // We forge and return the second splitted prefix. right = netip.MustParsePrefix( diff --git a/pkg/ipam/core/net_test.go b/pkg/ipam/core/net_test.go new file mode 100644 index 0000000000..e12deb6383 --- /dev/null +++ b/pkg/ipam/core/net_test.go @@ -0,0 +1,85 @@ +// Copyright 2019-2024 The Liqo 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 ipamcore + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ipam low level utilities ", func() { + Context("bit operations", func() { + When("setting bit in byte", func() { + It("should return 0", func() { + bytes := []byte{ + 0b10000000, + 0b01000000, + 0b00100000, + 0b00010000, + 0b00001000, + 0b00000100, + 0b00000010, + 0b00000001, + } + + var b byte = 0b00000000 + for i := range bytes { + r, err := setBit(b, i) + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(Equal(bytes[i])) + } + }) + + It("should return 255", func() { + bytes := []byte{ + 0b01111111, + 0b10111111, + 0b11011111, + 0b11101111, + 0b11110111, + 0b11111011, + 0b11111101, + 0b11111110, + } + + for i := range bytes { + r, err := setBit(bytes[i], i) + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(Equal(byte(0b11111111))) + } + }) + + It("should keep the byte unmodified", func() { + b := byte(0b00000000) + + r, err := setBit(b, 8) + Expect(r).To(Equal(b)) + Expect(err).To(HaveOccurred()) + + r, err = setBit(b, 9) + Expect(r).To(Equal(b)) + Expect(err).To(HaveOccurred()) + + r, err = setBit(b, -1) + Expect(r).To(Equal(b)) + Expect(err).To(HaveOccurred()) + + r, err = setBit(b, -2) + Expect(r).To(Equal(b)) + Expect(err).To(HaveOccurred()) + }) + }) + }) +}) diff --git a/pkg/ipam/core/node.go b/pkg/ipam/core/node.go index 4b1058eee9..12f8f6c13d 100644 --- a/pkg/ipam/core/node.go +++ b/pkg/ipam/core/node.go @@ -82,7 +82,7 @@ func allocateNetwork(size int, node *node) *netip.Prefix { } func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix { - if node.acquired || !node.prefix.Overlaps(prefix) { + if node.acquired || !isPrefixChildOf(node.prefix, prefix) { return nil } if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { @@ -105,16 +105,13 @@ func allocateNetworkWithPrefix(prefix netip.Prefix, node *node) *netip.Prefix { return allocateNetworkWithPrefix(prefix, node.right) } + // This should never happen return nil } func networkRelease(prefix netip.Prefix, node *node, gracePeriod time.Duration) *netip.Prefix { var result *netip.Prefix - if node == nil { - return nil - } - if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() && node.lastUpdateTimestamp.Add(gracePeriod).Before(time.Now()) { if node.acquired { @@ -124,11 +121,10 @@ func networkRelease(prefix netip.Prefix, node *node, gracePeriod time.Duration) } return nil } - - if node.left != nil && node.left.prefix.Overlaps(prefix) { + if node.left != nil && isPrefixChildOf(node.left.prefix, prefix) { result = networkRelease(prefix, node.left, gracePeriod) } - if node.right != nil && node.right.prefix.Overlaps(prefix) { + if node.right != nil && isPrefixChildOf(node.right.prefix, prefix) { result = networkRelease(prefix, node.right, gracePeriod) } @@ -138,10 +134,10 @@ func networkRelease(prefix netip.Prefix, node *node, gracePeriod time.Duration) func networkIsAvailable(prefix netip.Prefix, node *node) bool { if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { - if node.left != nil && node.left.left.isSplitted() { + if node.left != nil && (node.left.isSplitted() || node.left.acquired) { return false } - if node.right != nil && node.right.isSplitted() { + if node.right != nil && (node.right.isSplitted() || node.right.acquired) { return false } @@ -150,13 +146,13 @@ func networkIsAvailable(prefix netip.Prefix, node *node) bool { } if node.left == nil && node.right == nil { - return true + return !node.acquired } - if node.left != nil && node.left.prefix.Overlaps(prefix) && !node.left.acquired { + if node.left != nil && isPrefixChildOf(node.left.prefix, prefix) && !node.left.acquired { return networkIsAvailable(prefix, node.left) } - if node.right != nil && node.right.prefix.Overlaps(prefix) && !node.right.acquired { + if node.right != nil && isPrefixChildOf(node.right.prefix, prefix) && !node.right.acquired { return networkIsAvailable(prefix, node.right) } @@ -164,10 +160,6 @@ func networkIsAvailable(prefix netip.Prefix, node *node) bool { } func listNetworks(node *node) []netip.Prefix { - if node == nil { - return nil - } - if node.acquired { return []netip.Prefix{node.prefix} } @@ -267,18 +259,14 @@ func (n *node) ipRelease(ip netip.Addr, gracePeriod time.Duration) *netip.Addr { } func search(prefix netip.Prefix, node *node) *node { - if node == nil { - return nil - } - if node.prefix.Addr().Compare(prefix.Addr()) == 0 && node.prefix.Bits() == prefix.Bits() { return node } - if node.left != nil && node.left.prefix.Overlaps(prefix) { + if node.left != nil && isPrefixChildOf(node.left.prefix, prefix) { return search(prefix, node.left) } - if node.right != nil && node.right.prefix.Overlaps(prefix) { + if node.right != nil && isPrefixChildOf(node.right.prefix, prefix) { return search(prefix, node.right) } @@ -322,8 +310,6 @@ func (n *node) insert(nd nodeDirection, prefix netip.Prefix) { case rightDirection: n.right = &newNode return - default: - return } } @@ -357,9 +343,8 @@ func (n *node) next(direction nodeDirection) *node { return n.left case rightDirection: return n.right - default: - return nil } + return nil } func (n *node) toGraphviz() error { @@ -386,9 +371,6 @@ func (n *node) toGraphviz() error { } func (n *node) toGraphvizRecursive(sb *strings.Builder) { - if n == nil { - return - } label := n.prefix.String() if len(n.ips) > 0 { ipsString := []string{} diff --git a/pkg/ipam/initialize_test.go b/pkg/ipam/initialize_test.go index 75d19a63d6..8baed358f9 100644 --- a/pkg/ipam/initialize_test.go +++ b/pkg/ipam/initialize_test.go @@ -64,7 +64,11 @@ var _ = Describe("Initialize routine tests", func() { testutil.FakeNetwork("net6", testNamespace, "172.16.1.0/24", nil), ).Build() - ipamCore, err := ipamcore.NewIpam([]string{"10.0.0.0/8", "192.168.0.0/16", "172.16.1.0/24"}) + ipamCore, err := ipamcore.NewIpam([]netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("192.168.0.0/16"), + netip.MustParsePrefix("172.16.1.0/24"), + }) Expect(err).To(BeNil()) // Init ipam server diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index 7c0e71f6d2..b2ead93411 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -56,7 +56,16 @@ func New(ctx context.Context, cl client.Client, roots []string, opts *ServerOpti hs := health.NewServer() hs.SetServingStatus(IPAM_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_NOT_SERVING) - ipam, err := ipamcore.NewIpam(roots) + prefixRoots := make([]netip.Prefix, len(roots)) + for i, r := range roots { + p, err := netip.ParsePrefix(r) + if err != nil { + return nil, fmt.Errorf("failed to parse pool with prefix %q: %w", r, err) + } + prefixRoots[i] = p + } + + ipam, err := ipamcore.NewIpam(prefixRoots) if err != nil { return nil, err } diff --git a/pkg/ipam/sync_test.go b/pkg/ipam/sync_test.go index 4c601c9238..1f36e20cb7 100644 --- a/pkg/ipam/sync_test.go +++ b/pkg/ipam/sync_test.go @@ -72,7 +72,7 @@ var _ = Describe("Sync routine tests", func() { testutil.FakeNetwork("net4", testNamespace, "10.4.0.0/16", nil), ).Build() - ipamCore, err := ipamcore.NewIpam([]string{"10.0.0.0/8"}) + ipamCore, err := ipamcore.NewIpam([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}) Expect(err).To(BeNil()) // Populate the cache @@ -156,7 +156,7 @@ var _ = Describe("Sync routine tests", func() { testutil.FakeIP("ip3", testNamespace, "10.0.0.2", "10.0.0.0/24", nil, nil, false), ).Build() - ipamCore, err := ipamcore.NewIpam([]string{"10.0.0.0/8"}) + ipamCore, err := ipamcore.NewIpam([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}) Expect(err).To(BeNil()) // Populate the cache