Skip to content

Commit 886a511

Browse files
hsalluri259gandro
authored andcommitted
Release IPPrefixes: support AWS ENI IP Prefix unassignment
Cilium currently does not support releasing IPPrefixes and secondary IPs when IPPrefix delegation enabled due to which IP starvation is happening in our Kubernetes clusters. We have to manually delete a node in order to free unused IPPrefixes. With this fix, the operator will release those unused IPPrefixes and secondary IPs to reassign them again in AWS ENI. Added a no-op stub implementation in Alibaba and Azure Clouds to satisfy the NodeOperations interface. Fixes: cilium#32209, cilium#39904 Signed-off-by: Harish Salluri <hsalluri259@gmail.com>
1 parent 24c61cf commit 886a511

7 files changed

Lines changed: 178 additions & 118 deletions

File tree

pkg/alibabacloud/eni/node.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,13 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
377377
return r
378378
}
379379

380+
// ReleaseIPPrefixes is a no-op on AlibabaCloud since Alibaba ENIs don't
381+
// support prefix delegation.
382+
func (n *Node) ReleaseIPPrefixes(ctx context.Context, r *ipam.ReleaseAction) error {
383+
// nothing to do
384+
return nil
385+
}
386+
380387
// ReleaseIPs performs the ENI IP release operation
381388
func (n *Node) ReleaseIPs(ctx context.Context, r *ipam.ReleaseAction) error {
382389
return n.manager.api.UnassignPrivateIPAddresses(ctx, r.InterfaceID, r.IPsToRelease)
@@ -417,13 +424,6 @@ func (n *Node) IsPrefixDelegated() bool {
417424
return false
418425
}
419426

420-
func (n *Node) GetUsedIPWithPrefixes() int {
421-
if n.k8sObj == nil {
422-
return 0
423-
}
424-
return len(n.k8sObj.Status.IPAM.Used)
425-
}
426-
427427
// getLimits returns the interface and IP limits of this node
428428
func (n *Node) getLimits() (ipamTypes.Limits, bool) {
429429
n.mutex.RLock()

pkg/aws/eni/node.go

Lines changed: 130 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,84 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
154154
for _, eniId := range slices.Sorted(maps.Keys(n.enis)) {
155155
e := n.enis[eniId]
156156

157-
// IP release for prefixes is not currently supported. Will skip releasing from this ENI
158-
if len(e.Prefixes) > 0 {
157+
// Ignore if the ENI is not managed by Cilium
158+
if e.IsExcludedBySpec(n.k8sObj.Spec.ENI) {
159+
continue
160+
}
161+
ipPrefixes := e.Prefixes
162+
ipsOnENI := e.Addresses
163+
usedIPs := n.k8sObj.Status.IPAM.Used
164+
165+
matchedIPs := []string{}
166+
// Returns the first ENI with either IPPrefixes/secondary IPs to release instead of
167+
// looking for an ENI with max IPPrefixes/secondary IPs to release for faster and
168+
// lower latency when early ENIs are eligible.
169+
if len(ipPrefixes) > 0 {
170+
scopedLog.Debug(
171+
"Considering ENI for IPPrefix release",
172+
fieldEniID, e.ID,
173+
logfields.NeedIndex, *n.k8sObj.Spec.ENI.FirstInterfaceIndex,
174+
logfields.Index, e.Number,
175+
logfields.NumAddresses, len(e.Addresses),
176+
logfields.LenPrefixes, len(ipPrefixes),
177+
logfields.ExcessIPs, excessIPs,
178+
)
179+
180+
unusedIPPrefixes := []string{}
181+
if excessIPs >= option.ENIPDBlockSizeIPv4 {
182+
// Identify unused IP prefixes to release
183+
for _, prefix := range ipPrefixes {
184+
prefixAddr, err := netip.ParsePrefix(prefix)
185+
if err != nil {
186+
continue
187+
}
188+
189+
found := false
190+
for ip := range usedIPs {
191+
if prefixAddr.Contains(netip.MustParseAddr(ip)) {
192+
found = true
193+
break
194+
}
195+
}
196+
if !found {
197+
unusedIPPrefixes = append(unusedIPPrefixes, prefix)
198+
for _, ipStr := range e.Addresses {
199+
ip := netip.MustParseAddr(ipStr)
200+
if prefixAddr.Contains(ip) {
201+
matchedIPs = append(matchedIPs, ipStr)
202+
}
203+
}
204+
// Reduce excessIPs with option.ENIPDBlockSizeIPv4 number of IPs after adding a prefix
205+
excessIPs = excessIPs - option.ENIPDBlockSizeIPv4
206+
}
207+
208+
if excessIPs < option.ENIPDBlockSizeIPv4 {
209+
break
210+
}
211+
}
212+
}
213+
214+
secondaryIPs := getIndividualIPs(ipPrefixes, e.Addresses)
215+
if len(unusedIPPrefixes) > 0 || len(secondaryIPs) > 0 {
216+
r.InterfaceID = eniId
217+
r.PoolID = ipamTypes.PoolID(e.Subnet.ID)
218+
r.IPPrefixesToRelease = unusedIPPrefixes
219+
if len(secondaryIPs) > 0 {
220+
secondaryIPs = getUnusedIPs(usedIPs, secondaryIPs, e.IP)
221+
maxReleaseOnENI := min(excessIPs, len(secondaryIPs))
222+
matchedIPs = append(matchedIPs, secondaryIPs[:maxReleaseOnENI]...)
223+
}
224+
scopedLog.Debug(
225+
"ENI has unused secondary IPs and/or IPPrefixes that can be released",
226+
fieldEniID, e.ID,
227+
logfields.Prefix, unusedIPPrefixes,
228+
logfields.IPAddrs, matchedIPs,
229+
)
230+
r.IPsToRelease = matchedIPs
231+
// Return since we have either IPPrefixes/secondary IPs to release
232+
return r
233+
}
234+
// Look for next ENI if we do not have an ENI with either a Prefix/secondary IP to release
159235
continue
160236
}
161237
scopedLog.Debug(
@@ -166,22 +242,9 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
166242
logfields.NumAddresses, len(e.Addresses),
167243
)
168244

169-
if e.IsExcludedBySpec(n.k8sObj.Spec.ENI) {
170-
continue
171-
}
172-
173245
// Count free IP addresses on this ENI
174-
ipsOnENI := n.k8sObj.Status.ENI.ENIs[e.ID].Addresses
175-
freeIpsOnENI := []string{}
176-
for _, ip := range ipsOnENI {
177-
_, ipUsed := n.k8sObj.Status.IPAM.Used[ip]
178-
// exclude primary IPs
179-
if !ipUsed && ip != e.IP {
180-
freeIpsOnENI = append(freeIpsOnENI, ip)
181-
}
182-
}
246+
freeIpsOnENI := getUnusedIPs(usedIPs, ipsOnENI, e.IP)
183247
freeOnENICount := len(freeIpsOnENI)
184-
185248
if freeOnENICount <= 0 {
186249
continue
187250
}
@@ -193,7 +256,6 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
193256
logfields.FreeOnENICount, freeOnENICount,
194257
)
195258
maxReleaseOnENI := min(freeOnENICount, excessIPs)
196-
197259
firstENIWithFreeIPFound := r.IPsToRelease == nil
198260
eniWithMoreFreeIPsFound := maxReleaseOnENI > len(r.IPsToRelease)
199261
// Select the ENI with the most addresses available for release
@@ -203,12 +265,62 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
203265
r.IPsToRelease = freeIpsOnENI[:maxReleaseOnENI]
204266
}
205267
}
206-
207268
return r
208269
}
209270

271+
// ReleaseIPPrefixes performs the ENI IPPrefixes release operation
272+
func (n *Node) ReleaseIPPrefixes(ctx context.Context, r *ipam.ReleaseAction) error {
273+
if err := n.manager.api.UnassignENIPrefixes(ctx, r.InterfaceID, r.IPPrefixesToRelease); err != nil {
274+
return err
275+
}
276+
277+
return nil
278+
279+
}
280+
281+
// Get Unused Individual IPs
282+
func getUnusedIPs(usedIPs ipamTypes.AllocationMap, ipAddresses []string, primaryIP string) (unusedIPs []string) {
283+
for _, ipStr := range ipAddresses {
284+
_, usedIP := usedIPs[ipStr]
285+
if !usedIP && ipStr != primaryIP {
286+
unusedIPs = append(unusedIPs, ipStr)
287+
}
288+
}
289+
return unusedIPs
290+
}
291+
292+
// Get Individual IPs that do not belong to any IPPrefix
293+
func getIndividualIPs(ipPrefixes, ipAddresses []string) (individualIPs []string) {
294+
for _, ipStr := range ipAddresses {
295+
matched := false
296+
ip := netip.MustParseAddr(ipStr)
297+
for _, prefix := range ipPrefixes {
298+
prefixAddr, err := netip.ParsePrefix(prefix)
299+
if err != nil {
300+
continue
301+
}
302+
if prefixAddr.Contains(ip) {
303+
matched = true
304+
break
305+
}
306+
}
307+
if !matched {
308+
individualIPs = append(individualIPs, ipStr)
309+
}
310+
}
311+
return individualIPs
312+
}
313+
210314
// ReleaseIPs performs the ENI IP release operation
211315
func (n *Node) ReleaseIPs(ctx context.Context, r *ipam.ReleaseAction) error {
316+
// Filter IPs that do not belong to any IPPrefix
317+
if len(r.IPPrefixesToRelease) > 0 {
318+
r.IPsToRelease = getIndividualIPs(r.IPPrefixesToRelease, r.IPsToRelease)
319+
}
320+
321+
if len(r.IPsToRelease) <= 0 {
322+
return nil
323+
}
212324
if err := n.manager.api.UnassignPrivateIpAddresses(ctx, r.InterfaceID, r.IPsToRelease); err != nil {
213325
return err
214326
}
@@ -822,58 +934,6 @@ func (n *Node) IsPrefixDelegated() bool {
822934
return true
823935
}
824936

825-
// GetUsedIPWithPrefixes returns the total number of used IPs on the node including the prefixes allocated.
826-
// A prefix is considered as used if there is at least one allocated IP from that prefix. All IPs from a used prefix
827-
// are included in the count returned.
828-
func (n *Node) GetUsedIPWithPrefixes() int {
829-
var usedIps int
830-
eniPrefixes := make(map[string][]netip.Prefix)
831-
832-
// Populate ENI -> Prefix mapping
833-
for eniName, eni := range n.k8sObj.Status.ENI.ENIs {
834-
var prefixes []netip.Prefix
835-
for _, pfx := range eni.Prefixes {
836-
ipNet, err := netip.ParsePrefix(pfx)
837-
if err != nil {
838-
continue
839-
}
840-
prefixes = append(prefixes, ipNet)
841-
}
842-
eniPrefixes[eniName] = prefixes
843-
}
844-
usedPfx := make(map[netip.Prefix]bool)
845-
for ip, resource := range n.k8sObj.Status.IPAM.Used {
846-
// Fetch prefixes on this IP's ENI
847-
prefixNetworks, exists := eniPrefixes[resource.Resource]
848-
if !exists {
849-
continue
850-
}
851-
var prefixBased bool
852-
var pfx netip.Prefix
853-
addr, err := netip.ParseAddr(ip)
854-
if err != nil {
855-
continue
856-
}
857-
// Check if the IP is from any of the prefixes attached to this ENI
858-
for _, ipNet := range prefixNetworks {
859-
if ipNet.Contains(addr) {
860-
prefixBased = true
861-
pfx = ipNet
862-
break
863-
}
864-
}
865-
if prefixBased {
866-
if !usedPfx[pfx] {
867-
usedIps = usedIps + option.ENIPDBlockSizeIPv4
868-
usedPfx[pfx] = true
869-
}
870-
} else {
871-
usedIps++
872-
}
873-
}
874-
return usedIps
875-
}
876-
877937
// getEffectiveIPLimits computing the effective number of available addresses on the ENI
878938
// based on limits (which includes any left over prefix delegation capacity), as well as
879939
// just the left over prefix delegation capacity.

pkg/aws/eni/node_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,6 @@ func TestGetMaximumAllocatableIPv4(t *testing.T) {
4545
require.Equal(t, 0, n.GetMaximumAllocatableIPv4())
4646
}
4747

48-
// TestGetUsedIPWithPrefixes tests the logic computing used IPs on a node when prefix delegation is enabled.
49-
func TestGetUsedIPWithPrefixes(t *testing.T) {
50-
cn := newCiliumNode("node1", withInstanceType("m5a.large"))
51-
n := &Node{k8sObj: cn}
52-
eniName := "eni-1"
53-
prefixes := []string{"10.10.128.0/28", "10.10.128.16/28"}
54-
eniMap := make(map[string]types.ENI)
55-
eniMap[eniName] = types.ENI{Prefixes: prefixes}
56-
cn.Status.ENI.ENIs = eniMap
57-
58-
allocationMap := make(ipamTypes.AllocationMap)
59-
allocationMap["10.10.128.2"] = ipamTypes.AllocationIP{Resource: eniName}
60-
allocationMap["10.10.128.18"] = ipamTypes.AllocationIP{Resource: eniName}
61-
n.k8sObj.Status.IPAM.Used = allocationMap
62-
require.Equal(t, 32, n.GetUsedIPWithPrefixes())
63-
}
64-
6548
func Test_findSubnetInSameRouteTableWithNodeSubnet(t *testing.T) {
6649
routeTableMap := ipamTypes.RouteTableMap{
6750
"rt-1": &ipamTypes.RouteTable{

pkg/azure/ipam/node.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ func (n *Node) PrepareIPRelease(excessIPs int, scopedLog *slog.Logger) *ipam.Rel
5959
return &ipam.ReleaseAction{}
6060
}
6161

62+
// ReleaseIPPrefixes is a no-op on Azure since Azure ENIs don't
63+
// support prefix delegation.
64+
func (n *Node) ReleaseIPPrefixes(ctx context.Context, r *ipam.ReleaseAction) error {
65+
// nothing to do
66+
return nil
67+
}
68+
6269
// ReleaseIPs performs the IP release operation
6370
func (n *Node) ReleaseIPs(ctx context.Context, r *ipam.ReleaseAction) error {
6471
return fmt.Errorf("not implemented")
@@ -225,13 +232,6 @@ func (n *Node) IsPrefixDelegated() bool {
225232
return false
226233
}
227234

228-
func (n *Node) GetUsedIPWithPrefixes() int {
229-
if n.k8sObj == nil {
230-
return 0
231-
}
232-
return len(n.k8sObj.Status.IPAM.Used)
233-
}
234-
235235
// isAvailableInterface returns whether interface is available and the number of available IPs to allocate in interface
236236
func isAvailableInterface(requiredIfaceName string, iface *types.AzureInterface, scopedLog *slog.Logger) (availableOnInterface int, available bool) {
237237
if requiredIfaceName != "" {

0 commit comments

Comments
 (0)