diff --git a/cni/network/multitenancy.go b/cni/network/multitenancy.go index f39a429bd2..8c5f5fdcf5 100644 --- a/cni/network/multitenancy.go +++ b/cni/network/multitenancy.go @@ -242,7 +242,7 @@ func (m *Multitenancy) getNetworkContainersInternal( if err != nil && client.IsUnsupportedAPI(err) { ncConfig, errGetNC := m.cnsclient.GetNetworkContainer(ctx, orchestratorContext) if errGetNC != nil { - return nil, []net.IPNet{}, fmt.Errorf("%w", err) + return nil, []net.IPNet{}, fmt.Errorf("%w", errGetNC) } ncConfigs = append(ncConfigs, *ncConfig) } else if err != nil { diff --git a/cni/network/multitenancy_test.go b/cni/network/multitenancy_test.go index 0d1d66c82d..6e65f5994d 100644 --- a/cni/network/multitenancy_test.go +++ b/cni/network/multitenancy_test.go @@ -3,14 +3,18 @@ package network import ( "context" "encoding/json" + "errors" "net" "testing" "github.com/Azure/azure-container-networking/cni" "github.com/Azure/azure-container-networking/cns" + "github.com/Azure/azure-container-networking/cns/client" + "github.com/Azure/azure-container-networking/cns/types" "github.com/Azure/azure-container-networking/network" cniTypes "github.com/containernetworking/cni/pkg/types" cniTypesCurr "github.com/containernetworking/cni/pkg/types/100" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" ) @@ -42,7 +46,19 @@ type getAllNetworkContainersConfigurationHandler struct { err error } +type cnsAPIName string + +const ( + GetAllNetworkContainers cnsAPIName = "GetAllNetworkContainers" +) + +var ( + errUnsupportedAPI = errors.New("Unsupported API") + errNoOrchestratorContextFound = errors.New("No CNI OrchestratorContext Found") +) + type MockCNSClient struct { + unsupportedAPIs map[cnsAPIName]struct{} require *require.Assertions request requestIPAddressHandler release releaseIPAddressHandler @@ -61,12 +77,23 @@ func (c *MockCNSClient) ReleaseIPAddress(_ context.Context, ipconfig cns.IPConfi } func (c *MockCNSClient) GetNetworkContainer(ctx context.Context, orchestratorContext []byte) (*cns.GetNetworkContainerResponse, error) { - c.require.Exactly(c.getNetworkContainerConfiguration.orchestratorContext, orchestratorContext) + if !cmp.Equal(c.getNetworkContainerConfiguration.orchestratorContext, orchestratorContext) { + return nil, errNoOrchestratorContextFound + } return c.getNetworkContainerConfiguration.returnResponse, c.getNetworkContainerConfiguration.err } func (c *MockCNSClient) GetAllNetworkContainers(ctx context.Context, orchestratorContext []byte) ([]cns.GetNetworkContainerResponse, error) { - c.require.Exactly(c.getAllNetworkContainersConfiguration.orchestratorContext, orchestratorContext) + if _, isUnsupported := c.unsupportedAPIs[GetAllNetworkContainers]; isUnsupported { + e := &client.CNSClientError{} + e.Code = types.UnsupportedAPI + e.Err = errUnsupportedAPI + return nil, e + } + + if !cmp.Equal(c.getAllNetworkContainersConfiguration.orchestratorContext, orchestratorContext) { + return nil, errNoOrchestratorContextFound + } return c.getAllNetworkContainersConfiguration.returnResponse, c.getAllNetworkContainersConfiguration.err } @@ -456,7 +483,7 @@ func TestGetMultiTenancyCNIResult(t *testing.T) { tt.args.k8sNamespace, tt.args.ifName) if (err != nil) != tt.wantErr { - t.Errorf("GetContainerNetworkConfiguration() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetAllNetworkContainers() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { @@ -473,3 +500,287 @@ func TestGetMultiTenancyCNIResult(t *testing.T) { }) } } + +// TestGetMultiTenancyCNIResultUnsupportedAPI tests if new CNS API is not supported and old CNS API can handle to get ncConfig with "Unsupported API" error +func TestGetMultiTenancyCNIResultUnsupportedAPI(t *testing.T) { + require := require.New(t) //nolint:gocritic + + ncResponse := cns.GetNetworkContainerResponse{ + PrimaryInterfaceIdentifier: "10.0.0.0/16", + LocalIPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.0.0.5", + PrefixLength: 16, + }, + GatewayIPAddress: "", + }, + CnetAddressSpace: []cns.IPSubnet{ + { + IPAddress: "10.1.0.0", + PrefixLength: 16, + }, + }, + IPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.1.0.5", + PrefixLength: 16, + }, + DNSServers: nil, + GatewayIPAddress: "10.1.0.1", + }, + Routes: []cns.Route{ + { + IPAddress: "10.1.0.0/16", + GatewayIPAddress: "10.1.0.1", + }, + }, + } + + // set new CNS API is not supported + unsupportedAPIs := make(map[cnsAPIName]struct{}) + unsupportedAPIs["GetAllNetworkContainers"] = struct{}{} + + type args struct { + ctx context.Context + enableInfraVnet bool + nwCfg *cni.NetworkConfig + plugin *NetPlugin + k8sPodName string + k8sNamespace string + ifName string + } + + tests := []struct { + name string + args args + want *cns.GetNetworkContainerResponse + wantErr bool + }{ + { + name: "test happy path for Unsupported API with old CNS API", + args: args{ + enableInfraVnet: true, + nwCfg: &cni.NetworkConfig{ + MultiTenancy: true, + EnableSnatOnHost: true, + EnableExactMatchForPodName: true, + InfraVnetAddressSpace: "10.0.0.0/16", + IPAM: cni.IPAM{Type: "azure-vnet-ipam"}, + }, + plugin: &NetPlugin{ + ipamInvoker: NewMockIpamInvoker(false, false, false), + multitenancyClient: &Multitenancy{ + netioshim: &mockNetIOShim{}, + cnsclient: &MockCNSClient{ + unsupportedAPIs: unsupportedAPIs, + require: require, + getNetworkContainerConfiguration: getNetworkContainerConfigurationHandler{ + orchestratorContext: marshallPodInfo(cns.KubernetesPodInfo{ + PodName: "testpod", + PodNamespace: "testnamespace", + }), + returnResponse: &ncResponse, + }, + }, + }, + }, + k8sPodName: "testpod", + k8sNamespace: "testnamespace", + ifName: "eth0", + }, + want: &cns.GetNetworkContainerResponse{ + PrimaryInterfaceIdentifier: "10.0.0.0/16", + LocalIPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.0.0.5", + PrefixLength: 16, + }, + GatewayIPAddress: "", + }, + CnetAddressSpace: []cns.IPSubnet{ + { + IPAddress: "10.1.0.0", + PrefixLength: 16, + }, + }, + IPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.1.0.5", + PrefixLength: 16, + }, + DNSServers: nil, + GatewayIPAddress: "10.1.0.1", + }, + Routes: []cns.Route{ + { + IPAddress: "10.1.0.0/16", + GatewayIPAddress: "10.1.0.1", + }, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.plugin.multitenancyClient.GetAllNetworkContainers( + tt.args.ctx, + tt.args.nwCfg, + tt.args.k8sPodName, + tt.args.k8sNamespace, + tt.args.ifName) + if err != nil && tt.wantErr { + t.Fatalf("expected an error %+v but none received", err) + } + require.NoError(err) + require.Exactly(tt.want, got[0].ncResponse) + }) + } +} + +// TestGetMultiTenancyCNIResultNotFound test includes two sub test cases: +// 1. CNS supports new API and it does not have orchestratorContext info +// 2. CNS does not support new API and it does not have orchestratorContext info +func TestGetMultiTenancyCNIResultNotFound(t *testing.T) { + require := require.New(t) //nolint:gocritic + + ncResponse := cns.GetNetworkContainerResponse{ + PrimaryInterfaceIdentifier: "10.0.0.0/16", + LocalIPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.0.0.5", + PrefixLength: 16, + }, + GatewayIPAddress: "", + }, + CnetAddressSpace: []cns.IPSubnet{ + { + IPAddress: "10.1.0.0", + PrefixLength: 16, + }, + }, + IPConfiguration: cns.IPConfiguration{ + IPSubnet: cns.IPSubnet{ + IPAddress: "10.1.0.5", + PrefixLength: 16, + }, + DNSServers: nil, + GatewayIPAddress: "10.1.0.1", + }, + Routes: []cns.Route{ + { + IPAddress: "10.1.0.0/16", + GatewayIPAddress: "10.1.0.1", + }, + }, + } + + // set new CNS API is not supported + unsupportedAPIs := make(map[cnsAPIName]struct{}) + unsupportedAPIs["GetAllNetworkContainers"] = struct{}{} + + type args struct { + ctx context.Context + enableInfraVnet bool + nwCfg *cni.NetworkConfig + plugin *NetPlugin + k8sPodName string + k8sNamespace string + ifName string + } + + tests := []struct { + name string + args args + want *cns.GetNetworkContainerResponse + wantErr bool + }{ + { + name: "test happy path, CNS does not support new API without orchestratorContext found", + args: args{ + enableInfraVnet: true, + nwCfg: &cni.NetworkConfig{ + MultiTenancy: true, + EnableSnatOnHost: true, + EnableExactMatchForPodName: true, + InfraVnetAddressSpace: "10.0.0.0/16", + IPAM: cni.IPAM{Type: "azure-vnet-ipam"}, + }, + plugin: &NetPlugin{ + ipamInvoker: NewMockIpamInvoker(false, false, false), + multitenancyClient: &Multitenancy{ + netioshim: &mockNetIOShim{}, + cnsclient: &MockCNSClient{ + unsupportedAPIs: unsupportedAPIs, + require: require, + getNetworkContainerConfiguration: getNetworkContainerConfigurationHandler{ + orchestratorContext: marshallPodInfo(cns.KubernetesPodInfo{ + PodName: "testpod", + PodNamespace: "testnamespace", + }), + returnResponse: &ncResponse, + }, + }, + }, + }, + // use mismatched k8sPodName and k8sNamespace + k8sPodName: "testpod1", + k8sNamespace: "testnamespace1", + ifName: "eth0", + }, + wantErr: true, + }, + { + name: "test happy path, CNS does support new API without orchestratorContext found", + args: args{ + enableInfraVnet: true, + nwCfg: &cni.NetworkConfig{ + MultiTenancy: true, + EnableSnatOnHost: true, + EnableExactMatchForPodName: true, + InfraVnetAddressSpace: "10.0.0.0/16", + IPAM: cni.IPAM{Type: "azure-vnet-ipam"}, + }, + plugin: &NetPlugin{ + ipamInvoker: NewMockIpamInvoker(false, false, false), + multitenancyClient: &Multitenancy{ + netioshim: &mockNetIOShim{}, + cnsclient: &MockCNSClient{ + require: require, + getNetworkContainerConfiguration: getNetworkContainerConfigurationHandler{ + orchestratorContext: marshallPodInfo(cns.KubernetesPodInfo{ + PodName: "testpod", + PodNamespace: "testnamespace", + }), + returnResponse: &ncResponse, + }, + }, + }, + }, + // use mismatched k8sPodName and k8sNamespace + k8sPodName: "testpod1", + k8sNamespace: "testnamespace1", + ifName: "eth0", + }, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, err := tt.args.plugin.multitenancyClient.GetAllNetworkContainers( + tt.args.ctx, + tt.args.nwCfg, + tt.args.k8sPodName, + tt.args.k8sNamespace, + tt.args.ifName) + if err == nil && tt.wantErr { + t.Fatalf("expected an error %+v but none received", err) + } + + if !errors.Is(err, errNoOrchestratorContextFound) { + t.Fatalf("expected an error %s but %v received", errNoOrchestratorContextFound, err) + } + }) + } +}