diff --git a/pkg/controller/ipam/antrea_ipam_controller.go b/pkg/controller/ipam/antrea_ipam_controller.go index 3cc5173a52e..7a076d5bb42 100644 --- a/pkg/controller/ipam/antrea_ipam_controller.go +++ b/pkg/controller/ipam/antrea_ipam_controller.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "net" "strings" "time" @@ -253,7 +254,24 @@ func (c *AntreaIPAMController) cleanIPPoolForStatefulSet(namespacedName string) } // Find IP Pools annotated to StatefulSet via direct annotation or Namespace annotation -func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) []string { +func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) ([]string, []net.IP) { + + // Inspect IP annotation for the Pods + ipStrings, _ := ss.Spec.Template.Annotations[annotation.AntreaIPAMPodIPAnnotationKey] + ipStrings = strings.ReplaceAll(ipStrings, " ", "") + var ips []net.IP + if ipStrings != "" { + splittedIPStrings := strings.Split(ipStrings, annotation.AntreaIPAMAnnotationDelimiter) + for _, ipString := range splittedIPStrings { + ip := net.ParseIP(ipString) + if ipString != "" && ip == nil { + klog.Errorf("invalid IP annotation %s", ipStrings) + ips = nil + break + } + ips = append(ips, ip) + } + } // Inspect pool annotation for the Pods // In order to avoid extra API call in IPAM driver, IPAM annotations are defined @@ -261,7 +279,7 @@ func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) annotations, exists := ss.Spec.Template.Annotations[annotation.AntreaIPAMAnnotationKey] if exists { // Stateful Set Pod is annotated with dedicated IP pool - return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter) + return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter), ips } // Inspect Namespace @@ -269,15 +287,15 @@ func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) if err != nil { // Should never happen klog.Errorf("Namespace %s not found for StatefulSet %s", ss.Namespace, ss.Name) - return nil + return nil, nil } annotations, exists = namespace.Annotations[annotation.AntreaIPAMAnnotationKey] if exists { - return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter) + return strings.Split(annotations, annotation.AntreaIPAMAnnotationDelimiter), ips } - return nil + return nil, nil } @@ -287,7 +305,11 @@ func (c *AntreaIPAMController) getIPPoolsForStatefulSet(ss *appsv1.StatefulSet) func (c *AntreaIPAMController) preallocateIPPoolForStatefulSet(ss *appsv1.StatefulSet) error { klog.InfoS("Processing create notification", "Namespace", ss.Namespace, "StatefulSet", ss.Name) - ipPools := c.getIPPoolsForStatefulSet(ss) + ipPools, ips := c.getIPPoolsForStatefulSet(ss) + var ip net.IP + if len(ips) > 0 { + ip = ips[0] + } if ipPools == nil { // nothing to preallocate @@ -310,7 +332,7 @@ func (c *AntreaIPAMController) preallocateIPPoolForStatefulSet(ss *appsv1.Statef // in the pool. This safeguards us from double allocation in case agent allocated IP by the time // controller task is executed. Note also that StatefulSet resize will not be handled. if size > 0 { - err = allocator.AllocateStatefulSet(ss.Namespace, ss.Name, size) + err = allocator.AllocateStatefulSet(ss.Namespace, ss.Name, size, ip) if err != nil { return fmt.Errorf("failed to preallocate continuous IP space of size %d from Pool %s: %s", size, ipPoolName, err) } diff --git a/pkg/ipam/poolallocator/allocator.go b/pkg/ipam/poolallocator/allocator.go index e1e09965583..3319722c21c 100644 --- a/pkg/ipam/poolallocator/allocator.go +++ b/pkg/ipam/poolallocator/allocator.go @@ -435,7 +435,7 @@ func (a *IPPoolAllocator) AllocateReservedOrNext(state v1alpha2.IPAddressPhase, // This functionality is useful when StatefulSet does not have a dedicated IP Pool assigned. // It returns error if such range is not available. In this case IPs for the StatefulSet will // be allocated on the fly, and there is no guarantee for continuous IPs. -func (a *IPPoolAllocator) AllocateStatefulSet(namespace, name string, size int) error { +func (a *IPPoolAllocator) AllocateStatefulSet(namespace, name string, size int, ip net.IP) error { // Retry on CRD update conflict which is caused by multiple agents updating a pool at same time. err := retry.RetryOnConflict(retry.DefaultRetry, func() error { ipPool, allocators, err := a.getPoolAndInitIPAllocators() @@ -450,7 +450,13 @@ func (a *IPPoolAllocator) AllocateStatefulSet(namespace, name string, size int) } } - ips, err := allocators.AllocateRange(size) + var ips []net.IP + if size == 1 && ip != nil { + err = allocators.AllocateIP(ip) + ips = []net.IP{ip} + } else { + ips, err = allocators.AllocateRange(size) + } if err != nil { return err } diff --git a/pkg/ipam/poolallocator/allocator_test.go b/pkg/ipam/poolallocator/allocator_test.go index 6900ff8aa1c..7238352d130 100644 --- a/pkg/ipam/poolallocator/allocator_test.go +++ b/pkg/ipam/poolallocator/allocator_test.go @@ -421,7 +421,7 @@ func TestAllocateReleaseStatefulSet(t *testing.T) { } allocator := newTestIPPoolAllocator(&pool, stopCh) - err := allocator.AllocateStatefulSet(testNamespace, setName, 7) + err := allocator.AllocateStatefulSet(testNamespace, setName, 7, nil) require.NoError(t, err) // Make sure reserved IPs are respected for next allocate @@ -433,4 +433,20 @@ func TestAllocateReleaseStatefulSet(t *testing.T) { // Make sure reserved IPs are released validateAllocationSequence(t, allocator, subnetInfo, []string{"10.2.2.100"}) + + err = allocator.AllocateStatefulSet(testNamespace, setName, 1, net.ParseIP("10.2.2.101")) + require.NoError(t, err) + + // Make sure reserved IPs are respected for next allocate + validateAllocationSequence(t, allocator, subnetInfo, []string{"10.2.2.100", "10.2.2.102"}) + + // Release the set + err = allocator.ReleaseStatefulSet(testNamespace, setName) + require.NoError(t, err) + + // Make sure reserved IPs are released + validateAllocationSequence(t, allocator, subnetInfo, []string{"10.2.2.100"}) + + err = allocator.AllocateStatefulSet(testNamespace, setName, 1, net.ParseIP("10.2.3.101")) + require.Error(t, err) }