diff --git a/cni/network/network.go b/cni/network/network.go index fa22e798e7..d209e0cc07 100644 --- a/cni/network/network.go +++ b/cni/network/network.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "regexp" + "slices" "strconv" "time" @@ -640,6 +641,14 @@ func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error { } }() + // Sort epInfos so InfraNIC is processed first. 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, the network gets created with eth1 as extIf. + // SecondaryEndpointClient then moves eth1 into the pod namespace, and the subsequent InfraNIC + // iteration finds the network bound to that now-gone interface, causing + // TransparentEndpointClient.AddEndpoints to fail with "no such network interface". + sortInfraNICFirst(epInfos) + err = plugin.nm.EndpointCreate(cnsclient, epInfos) if err != nil { return errors.Wrap(err, "failed to create endpoint") // behavior can change if you don't assign to err prior to returning @@ -647,6 +656,26 @@ func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error { return nil } +// sortInfraNICFirst sorts endpoint infos so that InfraNIC (or legacy empty NICType) +// entries come before all other NIC types, preserving relative order among equals. +func sortInfraNICFirst(epInfos []*network.EndpointInfo) { + slices.SortStableFunc(epInfos, func(a, b *network.EndpointInfo) int { + if isInfraOrLegacyNICType(a.NICType) && !isInfraOrLegacyNICType(b.NICType) { + return -1 + } + if !isInfraOrLegacyNICType(a.NICType) && isInfraOrLegacyNICType(b.NICType) { + return 1 + } + return 0 + }) +} + +// isInfraOrLegacyNICType returns true if the NIC type is InfraNIC or empty (legacy). +// Empty NICType is treated as infra for backward compatibility with older CNS responses. +func isInfraOrLegacyNICType(nicType cns.NICType) bool { + return nicType == cns.InfraNIC || nicType == "" +} + func (plugin *NetPlugin) findMasterInterface(opt *createEpInfoOpt) string { switch opt.ifInfo.NICType { case cns.InfraNIC: @@ -1155,8 +1184,7 @@ func (plugin *NetPlugin) Delete(args *cniSkel.CmdArgs) error { zap.String("endpointID", epInfo.EndpointID)) telemetryClient.SendEvent("Deleting endpoint: " + epInfo.EndpointID) - isInfraOrLegacyNIC := epInfo.NICType == cns.InfraNIC || epInfo.NICType == "" - if !nwCfg.MultiTenancy && isInfraOrLegacyNIC { + if !nwCfg.MultiTenancy && isInfraOrLegacyNICType(epInfo.NICType) { // Call into IPAM plugin to release the endpoint's addresses. for i := range epInfo.IPAddresses { logger.Info("Release ip", zap.String("ip", epInfo.IPAddresses[i].IP.String())) diff --git a/cni/network/network_linux_test.go b/cni/network/network_linux_test.go index 3403447501..e67d225348 100644 --- a/cni/network/network_linux_test.go +++ b/cni/network/network_linux_test.go @@ -794,3 +794,43 @@ func (m *mockInterfaceGetter) GetNetworkInterfaces() ([]net.Interface, error) { func (m *mockInterfaceGetter) GetNetworkInterfaceAddrs(_ *net.Interface) ([]net.Addr, error) { return nil, errNotImplemented } + +func TestSortInfraNICFirst(t *testing.T) { + tests := []struct { + name string + input []cns.NICType + wantFirst cns.NICType + }{ + { + name: "infra already first", + input: []cns.NICType{cns.InfraNIC, cns.NodeNetworkInterfaceFrontendNIC, cns.NodeNetworkInterfaceFrontendNIC}, + wantFirst: cns.InfraNIC, + }, + { + name: "infra last among several frontends", + input: []cns.NICType{cns.NodeNetworkInterfaceFrontendNIC, cns.NodeNetworkInterfaceFrontendNIC, cns.NodeNetworkInterfaceFrontendNIC, cns.InfraNIC}, + wantFirst: cns.InfraNIC, + }, + { + name: "infra in the middle", + input: []cns.NICType{cns.DelegatedVMNIC, cns.InfraNIC, cns.NodeNetworkInterfaceFrontendNIC}, + wantFirst: cns.InfraNIC, + }, + { + name: "empty NICType treated as infra", + input: []cns.NICType{cns.NodeNetworkInterfaceFrontendNIC, ""}, + wantFirst: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + epInfos := make([]*network.EndpointInfo, len(tt.input)) + for i, nic := range tt.input { + epInfos[i] = &network.EndpointInfo{NICType: nic} + } + sortInfraNICFirst(epInfos) + assert.Equal(t, tt.wantFirst, epInfos[0].NICType, "infra/legacy NIC should be sorted first") + }) + } +}