Skip to content

Commit cabd410

Browse files
vcatlassiantklauser
authored andcommitted
Fix IPAM IP release racing condition
[ upstream commit 7fbf7c6 ] Racing condition when IP reassigned back to ENI, during IPAM release process. Signed-off-by: Victor Chen <victor.chenq@gmail.com> Signed-off-by: Tobias Klauser <tobias@cilium.io>
1 parent 1824704 commit cabd410

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

pkg/ipam/node.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,9 +758,19 @@ func (n *Node) abortNoLongerExcessIPs(excessMap map[string]bool) {
758758
}
759759
// Handshake can be aborted from every state except 'released'
760760
// 'released' state is removed by the agent once the IP has been removed from ciliumnode's IPAM pool as well.
761+
// But if the IP is back in the pool, we need to remove it from the release status map.
761762
if status == ipamOption.IPAMReleased {
763+
// Check if the IP is back in the pool despite being marked as released
764+
if _, ok := n.resource.Spec.IPAM.Pool[ip]; ok {
765+
delete(n.resource.Status.IPAM.ReleaseIPs, ip)
766+
delete(n.ipv4Alloc.ipsMarkedForRelease, ip)
767+
delete(n.ipv4Alloc.ipReleaseStatus, ip)
768+
}
769+
770+
// If it's still released and not in the pool, we don't need to do anything
762771
continue
763772
}
773+
764774
if status, ok := n.ipv4Alloc.ipReleaseStatus[ip]; ok && status != ipamOption.IPAMReleased {
765775
delete(n.ipv4Alloc.ipsMarkedForRelease, ip)
766776
delete(n.ipv4Alloc.ipReleaseStatus, ip)

pkg/ipam/node_manager_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
operatorOption "github.com/cilium/cilium/operator/option"
1818
"github.com/cilium/cilium/pkg/defaults"
1919
metricsmock "github.com/cilium/cilium/pkg/ipam/metrics/mock"
20+
ipamOption "github.com/cilium/cilium/pkg/ipam/option"
2021
ipamStats "github.com/cilium/cilium/pkg/ipam/stats"
2122
ipamTypes "github.com/cilium/cilium/pkg/ipam/types"
2223
v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
@@ -588,6 +589,139 @@ func TestNodeManagerAbortRelease(t *testing.T) {
588589
require.Equal(t, 3, node.Stats().IPv4.UsedIPs)
589590
}
590591

592+
// TestNodeManagerAbortReleaseIPReassignment tests a scenario where:
593+
// 1. An IP is released and marked as released in status.ipam.release-ips
594+
// 2. The IP is removed from spec.ipam.pool
595+
// 3. Before being removed from status.ipam.release-ips, the same IP is reassigned to the pool (ie. AWS ENI in the case of AWS ENI IPAM)
596+
// 4. The IP is no longer considered excess, so the release handshake is aborted and the IP is deleted from status.ipam.release-ips
597+
func TestNodeManagerAbortReleaseIPReassignment(t *testing.T) {
598+
operatorOption.Config.ExcessIPReleaseDelay = 2
599+
am := newAllocationImplementationMock()
600+
require.NotNil(t, am)
601+
mngr, err := NewNodeManager(am, k8sapi, metricsmock.NewMockMetrics(), 10, true, false)
602+
require.NoError(t, err)
603+
require.NotNil(t, mngr)
604+
605+
// Announce node, wait for IPs to become available
606+
cn := newCiliumNode("node4", 1, 3, 0)
607+
mngr.Upsert(cn)
608+
require.NoError(t, testutils.WaitUntil(func() bool { return reachedAddressesNeeded(mngr, "node4", 0) }, 5*time.Second))
609+
610+
node := mngr.Get("node4")
611+
require.NotNil(t, node)
612+
require.Equal(t, 3, node.Stats().IPv4.AvailableIPs)
613+
require.Equal(t, 0, node.Stats().IPv4.UsedIPs)
614+
615+
// Use 3 out of 4 IPs
616+
mngr.Upsert(updateCiliumNode(cn, 3))
617+
require.NoError(t, testutils.WaitUntil(func() bool { return reachedAddressesNeeded(mngr, "node4", 0) }, 5*time.Second))
618+
619+
node = mngr.Get("node4")
620+
require.NotNil(t, node)
621+
require.Equal(t, 4, node.Stats().IPv4.AvailableIPs)
622+
require.Equal(t, 3, node.Stats().IPv4.UsedIPs)
623+
624+
// Release one IP
625+
mngr.Upsert(updateCiliumNode(node.resource, 2))
626+
627+
node = mngr.Get("node4")
628+
require.NotNil(t, node)
629+
require.Equal(t, 4, node.Stats().IPv4.AvailableIPs)
630+
require.Equal(t, 2, node.Stats().IPv4.UsedIPs)
631+
632+
node.instanceSync.Trigger()
633+
634+
// This is the key IP that will be released and later reassigned
635+
var releasedIP string
636+
637+
// Wait for the IP to be marked as excess for longer than the ExcessIPReleaseDelay
638+
require.Eventually(t, func() bool {
639+
node.mutex.Lock()
640+
defer node.mutex.Unlock()
641+
642+
if len(node.ipv4Alloc.ipsMarkedForRelease) == 0 {
643+
return false
644+
}
645+
646+
for _, ts := range node.ipv4Alloc.ipsMarkedForRelease {
647+
if ts.Add(time.Duration(operatorOption.Config.ExcessIPReleaseDelay) * time.Second).Before(time.Now()) {
648+
return true
649+
}
650+
}
651+
652+
return false
653+
}, 10*time.Second, time.Second)
654+
655+
// Mark IP as excess
656+
node.instanceSync.Trigger()
657+
658+
// Wait for maintenance action to identify the IP to release
659+
require.Eventually(t, func() bool {
660+
a, err := node.determineMaintenanceAction()
661+
if err != nil || a == nil || a.release == nil || len(a.release.IPsToRelease) == 0 {
662+
return false
663+
}
664+
665+
// Get the IP being released
666+
releasedIP = a.release.IPsToRelease[0]
667+
return releasedIP != ""
668+
}, 10*time.Second, time.Second)
669+
670+
node.PopulateIPReleaseStatus(node.resource)
671+
672+
// Verify it's marked for release in the CiliumNode resource
673+
require.Contains(t, node.resource.Status.IPAM.ReleaseIPs, releasedIP)
674+
require.Equal(t, ipamOption.IPAMMarkForRelease, string(node.resource.Status.IPAM.ReleaseIPs[releasedIP]))
675+
676+
// Fake acknowledge IP for release like agent would
677+
testipam.FakeAcknowledgeReleaseIps(node.resource)
678+
679+
// Resync one more time to process acknowledgements.
680+
node.instanceSync.Trigger()
681+
682+
require.Eventually(t, func() bool {
683+
status, exists := node.resource.Status.IPAM.ReleaseIPs[releasedIP]
684+
return exists && string(status) == ipamOption.IPAMReadyForRelease
685+
}, 10*time.Second, time.Second)
686+
687+
// Now simulate the operator releasing the IP and marking it as released
688+
node.mutex.Lock()
689+
delete(node.resource.Spec.IPAM.Pool, releasedIP)
690+
node.resource.Status.IPAM.ReleaseIPs[releasedIP] = ipamOption.IPAMReleased
691+
node.mutex.Unlock()
692+
// Also mark it as released in the internal ipReleaseStatus map
693+
node.ipv4Alloc.ipReleaseStatus[releasedIP] = ipamOption.IPAMReleased
694+
695+
// Normally at this point, the agent would see the IP is released and remove it from Status.IPAM.ReleaseIPs
696+
// But before that happens, simulate the IP being reassigned back to the pool
697+
if node.resource.Spec.IPAM.Pool == nil {
698+
node.resource.Spec.IPAM.Pool = ipamTypes.AllocationMap{}
699+
}
700+
node.resource.Spec.IPAM.Pool[releasedIP] = ipamTypes.AllocationIP{Resource: "eni-test"}
701+
node.ops.AllocateIPs(context.Background(), &AllocationAction{
702+
IPv4: IPAllocationAction{
703+
AvailableForAllocation: 1,
704+
},
705+
})
706+
707+
node.poolMaintainer.Trigger()
708+
node.instanceSync.Trigger()
709+
710+
require.Eventually(t, func() bool {
711+
node.PopulateIPReleaseStatus(node.resource)
712+
_, inReleaseStatus := node.ipv4Alloc.ipReleaseStatus[releasedIP]
713+
_, inMarkedForRelease := node.ipv4Alloc.ipsMarkedForRelease[releasedIP]
714+
_, inReleaseIPs := node.resource.Status.IPAM.ReleaseIPs[releasedIP]
715+
716+
return !inReleaseStatus && !inMarkedForRelease && !inReleaseIPs
717+
}, 10*time.Second, time.Second)
718+
719+
node = mngr.Get("node4")
720+
require.NotNil(t, node)
721+
require.Equal(t, 4, node.Stats().IPv4.AvailableIPs)
722+
require.Equal(t, 2, node.Stats().IPv4.UsedIPs)
723+
}
724+
591725
type nodeState struct {
592726
cn *v2.CiliumNode
593727
name string

0 commit comments

Comments
 (0)