Skip to content

Commit c665385

Browse files
behzad-mirCopilot
andcommitted
fix: sort epInfos to process InfraNIC first before EndpointCreate
Move the epInfos sort from network.EndpointCreate to the CNI plugin layer (cni/network/network.go) after the createEpInfo loop. In stateless mode, ExternalInterfaces is empty on every ADD so whichever NIC is processed first determines which interface the network gets bound to. If DelegatedVMNIC wins the race, SecondaryEndpointClient moves its interface into the pod namespace, then TransparentEndpointClient fails with "no such network interface". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d892ddc commit c665385

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

cni/network/network.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"net"
1111
"regexp"
12+
"slices"
1213
"strconv"
1314
"time"
1415

@@ -640,6 +641,24 @@ func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error {
640641
}
641642
}()
642643

644+
// Sort epInfos so InfraNIC is processed first. In stateless mode, ExternalInterfaces is empty
645+
// on every ADD, so whichever NIC is processed first determines which interface the network
646+
// gets bound to. If DelegatedVMNIC wins the race, the network gets created with eth1 as extIf.
647+
// SecondaryEndpointClient then moves eth1 into the pod namespace, and the subsequent InfraNIC
648+
// iteration finds the network bound to that now-gone interface, causing
649+
// TransparentEndpointClient.AddEndpoints to fail with "no such network interface".
650+
slices.SortStableFunc(epInfos, func(a, b *network.EndpointInfo) int {
651+
aIsInfra := a.NICType == cns.InfraNIC || a.NICType == ""
652+
bIsInfra := b.NICType == cns.InfraNIC || b.NICType == ""
653+
if aIsInfra && !bIsInfra {
654+
return -1
655+
}
656+
if !aIsInfra && bIsInfra {
657+
return 1
658+
}
659+
return 0
660+
})
661+
643662
err = plugin.nm.EndpointCreate(cnsclient, epInfos)
644663
if err != nil {
645664
return errors.Wrap(err, "failed to create endpoint") // behavior can change if you don't assign to err prior to returning

cni/network/network_linux_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net"
1010
"regexp"
11+
"slices"
1112
"testing"
1213

1314
"github.com/Azure/azure-container-networking/cni"
@@ -794,3 +795,41 @@ func (m *mockInterfaceGetter) GetNetworkInterfaces() ([]net.Interface, error) {
794795
func (m *mockInterfaceGetter) GetNetworkInterfaceAddrs(_ *net.Interface) ([]net.Addr, error) {
795796
return nil, errNotImplemented
796797
}
798+
799+
func TestSortEpInfosInfraNICFirst(t *testing.T) {
800+
// Replicate the sort logic used before EndpointCreate to verify InfraNIC
801+
// is always processed first regardless of input order.
802+
epInfos := []*network.EndpointInfo{
803+
{
804+
NICType: cns.DelegatedVMNIC,
805+
EndpointID: "delegated-ep",
806+
NetworkID: network.DefaultNetworkID,
807+
},
808+
{
809+
NICType: cns.InfraNIC,
810+
EndpointID: "infra-ep",
811+
NetworkID: network.DefaultNetworkID,
812+
},
813+
{
814+
NICType: cns.NodeNetworkInterfaceFrontendNIC,
815+
EndpointID: "frontend-ep",
816+
NetworkID: "other",
817+
},
818+
}
819+
820+
slices.SortStableFunc(epInfos, func(a, b *network.EndpointInfo) int {
821+
aIsInfra := a.NICType == cns.InfraNIC || a.NICType == ""
822+
bIsInfra := b.NICType == cns.InfraNIC || b.NICType == ""
823+
if aIsInfra && !bIsInfra {
824+
return -1
825+
}
826+
if !aIsInfra && bIsInfra {
827+
return 1
828+
}
829+
return 0
830+
})
831+
832+
assert.Equal(t, cns.InfraNIC, epInfos[0].NICType, "InfraNIC should be sorted first")
833+
assert.Equal(t, cns.DelegatedVMNIC, epInfos[1].NICType, "DelegatedVMNIC should come after InfraNIC")
834+
assert.Equal(t, cns.NodeNetworkInterfaceFrontendNIC, epInfos[2].NICType, "FrontendNIC should come last")
835+
}

0 commit comments

Comments
 (0)