diff --git a/ecs-agent/netlib/common_test.go b/ecs-agent/netlib/common_test.go index 73647040b1c..1ed91b65154 100644 --- a/ecs-agent/netlib/common_test.go +++ b/ecs-agent/netlib/common_test.go @@ -67,20 +67,10 @@ func getSingleNetNSAWSVPCTestData(testTaskID string) (*ecsacs.Task, tasknetworkc netNSName := fmt.Sprintf(netNSNamePattern, testTaskID, eniName) netNSPath := netNSPathDir + netNSName + netNS, _ := tasknetworkconfig.NewNetworkNamespace(netNSName, netNSPath, 0, nil, &netIfs[0]) taskNetConfig := tasknetworkconfig.TaskNetworkConfig{ - NetworkMode: types.NetworkModeAwsvpc, - NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{ - { - Name: netNSName, - Path: netNSPath, - Index: 0, - NetworkInterfaces: []*networkinterface.NetworkInterface{ - &netIfs[0], - }, - KnownState: status.NetworkNone, - DesiredState: status.NetworkReadyPull, - }, - }, + NetworkMode: types.NetworkModeAwsvpc, + NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{netNS}, } return taskPayload, taskNetConfig @@ -152,30 +142,12 @@ func getMultiNetNSMultiIfaceAWSVPCTestData(testTaskID string) (*ecsacs.Task, tas secondaryNetNSName := fmt.Sprintf(netNSNamePattern, testTaskID, ifName2) secondaryNetNSPath := netNSPathDir + secondaryNetNSName + primaryNetNS, _ := tasknetworkconfig.NewNetworkNamespace(primaryNetNSName, primaryNetNSPath, 0, nil, &netIfs[0]) + secondaryNetNS, _ := tasknetworkconfig.NewNetworkNamespace(secondaryNetNSName, secondaryNetNSPath, 1, nil, &netIfs[1]) + taskNetConfig := tasknetworkconfig.TaskNetworkConfig{ - NetworkMode: types.NetworkModeAwsvpc, - NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{ - { - Name: primaryNetNSName, - Path: primaryNetNSPath, - Index: 0, - NetworkInterfaces: []*networkinterface.NetworkInterface{ - &netIfs[0], - }, - KnownState: status.NetworkNone, - DesiredState: status.NetworkReadyPull, - }, - { - Name: secondaryNetNSName, - Path: secondaryNetNSPath, - Index: 1, - NetworkInterfaces: []*networkinterface.NetworkInterface{ - &netIfs[1], - }, - KnownState: status.NetworkNone, - DesiredState: status.NetworkReadyPull, - }, - }, + NetworkMode: types.NetworkModeAwsvpc, + NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{primaryNetNS, secondaryNetNS}, } return taskPayload, taskNetConfig @@ -323,6 +295,7 @@ func getV2NTestData(testTaskID string) (*ecsacs.Task, tasknetworkconfig.TaskNetw Name: netNSName, Path: netNSPath, Index: 0, + NetworkMode: types.NetworkModeAwsvpc, NetworkInterfaces: netIfs, KnownState: status.NetworkNone, DesiredState: status.NetworkReadyPull, diff --git a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go index efd5e8555da..df32f7626c6 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go @@ -22,6 +22,7 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/serviceconnect" "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status" + "github.com/aws/aws-sdk-go-v2/service/ecs/types" ) // NetworkNamespace is model representing each network namespace. @@ -30,6 +31,10 @@ type NetworkNamespace struct { Path string Index int + // NetworkMode represents the network mode for this namespace. + // Supported values: awsvpc (default), daemon-bridge (managed-instances only). + NetworkMode types.NetworkMode + // NetworkInterfaces represents ENIs or any kind of network interface associated the particular netns. NetworkInterfaces []*networkinterface.NetworkInterface @@ -58,6 +63,7 @@ func NewNetworkNamespace( NetworkInterfaces: networkInterfaces, KnownState: status.NetworkNone, DesiredState: status.NetworkReadyPull, + NetworkMode: types.NetworkModeAwsvpc, } // Sort interfaces as per their index values in ascending order. @@ -104,3 +110,9 @@ func (ns *NetworkNamespace) GetInterfaceByIndex(idx int64) *networkinterface.Net return nil } + +// WithNetworkMode sets the NetworkMode field +func (ns *NetworkNamespace) WithNetworkMode(mode types.NetworkMode) *NetworkNamespace { + ns.NetworkMode = mode + return ns +} diff --git a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go index 19987d69d6d..aca68a53501 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go @@ -47,6 +47,7 @@ func TestNewNetworkNamespace(t *testing.T) { assert.Equal(t, primaryNetNSName, netns.Name) assert.Equal(t, primaryNetNSPath, netns.Path) assert.Equal(t, 0, netns.Index) + assert.Equal(t, "awsvpc", string(netns.NetworkMode)) assert.Empty(t, netns.AppMeshConfig) assert.Equal(t, *netIFs[0], *netns.NetworkInterfaces[0]) assert.Equal(t, *netIFs[1], *netns.NetworkInterfaces[1]) @@ -78,3 +79,10 @@ func TestNetworkNamespace_IsPrimary(t *testing.T) { require.Equal(t, tc.isPrimary, tc.netNS.IsPrimary()) } } + +func TestNetworkNamespace_WithNetworkMode(t *testing.T) { + netns := &NetworkNamespace{} + result := netns.WithNetworkMode("daemon-bridge") + assert.Equal(t, "daemon-bridge", string(result.NetworkMode)) + assert.Equal(t, netns, result) // Should return same instance +} diff --git a/ecs-agent/netlib/network_builder.go b/ecs-agent/netlib/network_builder.go index 4783aafa3b3..4aea797c729 100644 --- a/ecs-agent/netlib/network_builder.go +++ b/ecs-agent/netlib/network_builder.go @@ -102,6 +102,8 @@ func (nb *networkBuilder) Start( err = nb.startAWSVPC(ctx, taskID, netNS) case types.NetworkModeHost: err = nb.platformAPI.HandleHostMode() + case "daemon-bridge": + err = nb.platformAPI.ConfigureDaemonNetNS(netNS) default: err = errors.New("invalid network mode: " + string(mode)) } @@ -132,6 +134,10 @@ func (nb *networkBuilder) Stop(ctx context.Context, mode types.NetworkMode, task err = nb.stopAWSVPC(ctx, netNS) case types.NetworkModeHost: err = nb.platformAPI.HandleHostMode() + case "daemon-bridge": + // Adding extra logging to help with debug TODO remove later. + logger.Info("Stopping Daemon network namespace setup", logFields) + err = nb.platformAPI.StopDaemonNetNS(ctx, netNS) default: err = errors.New("invalid network mode: " + string(mode)) } diff --git a/ecs-agent/netlib/network_builder_linux_test.go b/ecs-agent/netlib/network_builder_linux_test.go index 4d171a0c17d..90d8afafad5 100644 --- a/ecs-agent/netlib/network_builder_linux_test.go +++ b/ecs-agent/netlib/network_builder_linux_test.go @@ -70,11 +70,13 @@ func TestNetworkBuilder_BuildTaskNetworkConfiguration(t *testing.T) { func TestNetworkBuilder_Start(t *testing.T) { t.Run("awsvpc", testNetworkBuilder_StartAWSVPC) + t.Run("daemon-bridge", testNetworkBuilder_StartDaemonBridge) } // TestNetworkBuilder_Stop verifies stop workflow for AWSVPC mode. func TestNetworkBuilder_Stop(t *testing.T) { t.Run("awsvpc", testNetworkBuilder_StopAWSVPC) + t.Run("daemon-bridge", testNetworkBuilder_StopDaemonBridge) } // getTestFunc returns a test function that verifies the capability of the networkBuilder @@ -380,3 +382,66 @@ func getExpectedCalls_StopAWSVPC( platformAPI.EXPECT().DeleteDNSConfig(netNS.Name).Return(nil).Times(1), platformAPI.EXPECT().DeleteNetNS(netNS.Path).Return(nil).Times(1)) } + +// testNetworkBuilder_StartDaemonBridge verifies that the expected platform API calls +// are made by the network builder while configuring daemon-bridge network namespace. +func testNetworkBuilder_StartDaemonBridge(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.TODO() + platformAPI := mock_platform.NewMockAPI(ctrl) + metricsFactory := mock_metrics.NewMockEntryFactory(ctrl) + mockEntry := mock_metrics.NewMockEntry(ctrl) + netBuilder := &networkBuilder{ + platformAPI: platformAPI, + metricsFactory: metricsFactory, + } + + // Create a daemon-bridge network namespace + netNS, _ := tasknetworkconfig.NewNetworkNamespace("daemon-ns", "/var/run/netns/daemon-ns", 0, nil) + netNS = netNS.WithNetworkMode("daemon-bridge") + netNS.KnownState = status.NetworkNone + netNS.DesiredState = status.NetworkReadyPull + + t.Run("daemon-bridge-start", func(*testing.T) { + gomock.InOrder( + metricsFactory.EXPECT().New(metrics.BuildNetworkNamespaceMetricName).Return(mockEntry).Times(1), + mockEntry.EXPECT().WithFields(gomock.Any()).Return(mockEntry).Times(1), + platformAPI.EXPECT().ConfigureDaemonNetNS(netNS).Return(nil).Times(1), + mockEntry.EXPECT().Done(nil).Times(1), + ) + netBuilder.Start(ctx, "daemon-bridge", taskID, netNS) + }) +} + +// testNetworkBuilder_StopDaemonBridge verifies that the cleanup of daemon-bridge +// network namespace works as expected. +func testNetworkBuilder_StopDaemonBridge(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.TODO() + platformAPI := mock_platform.NewMockAPI(ctrl) + metricsFactory := mock_metrics.NewMockEntryFactory(ctrl) + mockEntry := mock_metrics.NewMockEntry(ctrl) + netBuilder := &networkBuilder{ + platformAPI: platformAPI, + metricsFactory: metricsFactory, + } + + // Create a daemon-bridge network namespace + netNS, _ := tasknetworkconfig.NewNetworkNamespace("daemon-ns", "/var/run/netns/daemon-ns", 0, nil) + netNS = netNS.WithNetworkMode("daemon-bridge") + netNS.DesiredState = status.NetworkDeleted + + t.Run("daemon-bridge-stop", func(*testing.T) { + gomock.InOrder( + metricsFactory.EXPECT().New(metrics.DeleteNetworkNamespaceMetricName).Return(mockEntry).Times(1), + mockEntry.EXPECT().WithFields(gomock.Any()).Return(mockEntry).Times(1), + platformAPI.EXPECT().StopDaemonNetNS(ctx, netNS).Return(nil).Times(1), + mockEntry.EXPECT().Done(nil).Times(1), + ) + netBuilder.Stop(ctx, "daemon-bridge", taskID, netNS) + }) +} diff --git a/ecs-agent/netlib/platform/api.go b/ecs-agent/netlib/platform/api.go index 02fe7e809d1..45eb5d9ff54 100644 --- a/ecs-agent/netlib/platform/api.go +++ b/ecs-agent/netlib/platform/api.go @@ -78,6 +78,14 @@ type API interface { primaryIf *networkinterface.NetworkInterface, scConfig *serviceconnect.ServiceConnectConfig, ) error + + // ConfigureDaemonNetNS configures a network namespace for workloads running as daemons. + // This is an internal networking mode available in EMI (ECS Managed Instances) only. + ConfigureDaemonNetNS(netNS *tasknetworkconfig.NetworkNamespace) error + + // StopDaemonNetNS stops and cleans up a daemon network namespace. + // This is an internal networking mode available in EMI (ECS Managed Instances) only. + StopDaemonNetNS(ctx context.Context, netNS *tasknetworkconfig.NetworkNamespace) error } // Config contains platform-specific data. diff --git a/ecs-agent/netlib/platform/cniconf_linux.go b/ecs-agent/netlib/platform/cniconf_linux.go index 44336ca4309..de95a4f7543 100644 --- a/ecs-agent/netlib/platform/cniconf_linux.go +++ b/ecs-agent/netlib/platform/cniconf_linux.go @@ -53,7 +53,8 @@ const ( VPCTunnelInterfaceTypeGeneve = "geneve" VPCTunnelInterfaceTypeTap = "tap" - BridgeInterfaceName = "fargate-bridge" + BridgeInterfaceName = "fargate-bridge" + ManagedInstanceBridgeName = "mi-bridge" IPAMDataFileName = "eni-ipam.db" @@ -121,6 +122,40 @@ func createBridgePluginConfig(netNSPath string) ecscni.PluginConfig { return bridgeConfig } +// createBridgePluginConfig constructs the configuration object for bridge plugin +func createDaemonBridgePluginConfig(netNSPath string) ecscni.PluginConfig { + cniConfig := ecscni.CNIConfig{ + NetNSPath: netNSPath, + CNISpecVersion: cniSpecVersion, + CNIPluginName: BridgePluginName, + } + + _, routeIPNet, _ := net.ParseCIDR(AgentEndpoint) + route := &types.Route{ + Dst: *routeIPNet, + } + + ipamConfig := &ecscni.IPAMConfig{ + CNIConfig: ecscni.CNIConfig{ + NetNSPath: netNSPath, + CNISpecVersion: cniSpecVersion, + CNIPluginName: IPAMPluginName, + }, + IPV4Subnet: ECSSubNet, + IPV4Routes: []*types.Route{route}, + ID: netNSPath, + } + + // Invoke the bridge plugin and ipam plugin + bridgeConfig := &ecscni.BridgeConfig{ + CNIConfig: cniConfig, + Name: ManagedInstanceBridgeName, + IPAM: *ipamConfig, + } + + return bridgeConfig +} + func createAppMeshPluginConfig( netNSPath string, cfg *appmesh.AppMesh, diff --git a/ecs-agent/netlib/platform/cniconf_linux_test.go b/ecs-agent/netlib/platform/cniconf_linux_test.go index de3dbb477ba..3b306f52f69 100644 --- a/ecs-agent/netlib/platform/cniconf_linux_test.go +++ b/ecs-agent/netlib/platform/cniconf_linux_test.go @@ -79,6 +79,44 @@ func TestCreateBridgeConfig(t *testing.T) { require.Equal(t, expected, actual) } +func TestCreateDaemonBridgeConfig(t *testing.T) { + cniConfig := ecscni.CNIConfig{ + NetNSPath: netNSPath, + CNISpecVersion: cniSpecVersion, + CNIPluginName: BridgePluginName, + } + + _, routeIPNet, _ := net.ParseCIDR(AgentEndpoint) + route := &types.Route{ + Dst: *routeIPNet, + } + + ipamConfig := &ecscni.IPAMConfig{ + CNIConfig: ecscni.CNIConfig{ + NetNSPath: netNSPath, + CNISpecVersion: cniSpecVersion, + CNIPluginName: IPAMPluginName, + }, + IPV4Subnet: ECSSubNet, + IPV4Routes: []*types.Route{route}, + ID: netNSPath, + } + + // Invoke the bridge plugin and ipam plugin + bridgeConfig := &ecscni.BridgeConfig{ + CNIConfig: cniConfig, + Name: ManagedInstanceBridgeName, + IPAM: *ipamConfig, + } + + expected, err := json.Marshal(bridgeConfig) + require.NoError(t, err) + actual, err := json.Marshal(createDaemonBridgePluginConfig(netNSPath)) + require.NoError(t, err) + + require.Equal(t, expected, actual) +} + func TestCreateENIConfig(t *testing.T) { for _, tc := range []struct { name string diff --git a/ecs-agent/netlib/platform/common.go b/ecs-agent/netlib/platform/common.go index d00bdac08e2..9173d892c7e 100644 --- a/ecs-agent/netlib/platform/common.go +++ b/ecs-agent/netlib/platform/common.go @@ -19,6 +19,7 @@ import ( "time" "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/ecscni" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" "github.com/containernetworking/cni/pkg/types" ) @@ -92,3 +93,15 @@ func (c *common) interfacesMACToName() (map[string]string, error) { func (c *common) HandleHostMode() error { return errors.New("invalid platform for host mode") } + +// ConfigureDaemonNetNS configures a network namespace for workloads running as daemons. +// This is an internal networking mode available in EMI (ECS Managed Instances) only. +func (c *common) ConfigureDaemonNetNS(netNS *tasknetworkconfig.NetworkNamespace) error { + return errors.New("daemon network namespaces are not supported in this platform") +} + +// StopDaemonNetNS stops and cleans up a daemon network namespace. +// This is an internal networking mode available in EMI (ECS Managed Instances) only. +func (c *common) StopDaemonNetNS(ctx context.Context, netNS *tasknetworkconfig.NetworkNamespace) error { + return errors.New("daemon network namespaces are not supported in this platform") +} diff --git a/ecs-agent/netlib/platform/containerd_windows.go b/ecs-agent/netlib/platform/containerd_windows.go index d7203763b7d..396424eb86c 100644 --- a/ecs-agent/netlib/platform/containerd_windows.go +++ b/ecs-agent/netlib/platform/containerd_windows.go @@ -141,9 +141,10 @@ func (c *containerd) buildAWSVPCNetworkConfig( } netNS := &tasknetworkconfig.NetworkNamespace{ - Name: netNSName, - Path: netNSPath, - Index: 0, + Name: netNSName, + Path: netNSPath, + Index: 0, + NetworkMode: ecstypes.NetworkModeAwsvpc, NetworkInterfaces: []*networkinterface.NetworkInterface{ iface, }, diff --git a/ecs-agent/netlib/platform/managed_linux.go b/ecs-agent/netlib/platform/managed_linux.go index 2d2b7ca62b2..3081423bda6 100644 --- a/ecs-agent/netlib/platform/managed_linux.go +++ b/ecs-agent/netlib/platform/managed_linux.go @@ -56,10 +56,15 @@ func (m *managedLinux) BuildTaskNetworkConfiguration( return nil, errors.Wrap(err, "failed to translate network configuration") } case types.NetworkModeHost: - netNSs, err = m.buildDefaultNetworkNamespace(taskID) + netNSs, err = m.buildDefaultNetworkNamespaceConfig(taskID) if err != nil { return nil, errors.Wrap(err, "failed to create network namespace with host eni") } + case "daemon-bridge": + netNSs, err = m.buildHostDaemonNamespaceConfig(taskID) + if err != nil { + return nil, errors.Wrap(err, "failed to create daemon host namespace") + } default: return nil, errors.New("invalid network mode: " + string(mode)) } @@ -205,7 +210,7 @@ func (m *managedLinux) ConfigureServiceConnect( } // buildDefaultNetworkNamespace return default network namespace of host ENI for host mode. -func (m *managedLinux) buildDefaultNetworkNamespace(taskID string) ([]*tasknetworkconfig.NetworkNamespace, error) { +func (m *managedLinux) buildDefaultNetworkNamespaceConfig(taskID string) ([]*tasknetworkconfig.NetworkNamespace, error) { macAddress, err1 := m.client.GetMetadata(MacResource) ec2ID, err2 := m.client.GetMetadata(InstanceIDResource) macToNames, err3 := m.common.interfacesMACToName() @@ -306,3 +311,164 @@ func (m *managedLinux) buildDefaultNetworkNamespace(taskID string) ([]*tasknetwo func (m *managedLinux) HandleHostMode() error { return nil } + +func (m *managedLinux) buildHostDaemonNamespaceConfig(taskID string) ([]*tasknetworkconfig.NetworkNamespace, error) { + macAddress, err1 := m.client.GetMetadata(MacResource) + ec2ID, err2 := m.client.GetMetadata(InstanceIDResource) + macToNames, err3 := m.common.interfacesMACToName() + if err := goErr.Join(err1, err2, err3); err != nil { + logger.Error("Error fetching fields for default ENI", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + + hostENI := &ecsacs.ElasticNetworkInterface{ + AttachmentArn: aws.String("arn"), + Ec2Id: aws.String(ec2ID), + MacAddress: aws.String(macAddress), + DomainNameServers: []*string{}, + DomainName: []*string{}, + PrivateDnsName: aws.String(DefaultArg), + InterfaceAssociationProtocol: aws.String(DefaultArg), + Index: aws.Int64(64), + } + + ipComp, err := net.DetermineIPCompatibility(m.netlink, macAddress) + if err != nil { + logger.Error("Failed to determine IP compatibility of host ENI", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + + if !ipComp.IsIPv4Compatible() && !ipComp.IsIPv6Compatible() { + return nil, errors.New("Failed to build the default network namespace because the host ENI is neither " + + "IPv4 enabled nor IPv6 enabled") + } + + if ipComp.IsIPv6Compatible() { + privateIpv6, err1 := m.client.GetMetadata(PrivateIPv6Address) + ipv6SubNet, err2 := m.client.GetMetadata(fmt.Sprintf(IPv6SubNetCidrBlock, macAddress)) + if err := goErr.Join(err1, err2); err != nil { + logger.Error("Error fetching IPv6 fields for default ENI", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + + hostENI.Ipv6Addresses = []*ecsacs.IPv6AddressAssignment{ + { + Primary: aws.Bool(true), + Address: aws.String(privateIpv6), + }, + } + hostENI.SubnetGatewayIpv6Address = aws.String(ipv6SubNet) + } + + if ipComp.IsIPv4Compatible() { + privateIpv4, err1 := m.client.GetMetadata(PrivateIPv4Address) + ipv4SubNet, err2 := m.client.GetMetadata(fmt.Sprintf(IPv4SubNetCidrBlock, macAddress)) + if err := goErr.Join(err1, err2); err != nil { + logger.Error("Error fetching IPv4 fields for default ENI", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + + hostENI.Ipv4Addresses = []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(privateIpv4), + }, + } + hostENI.SubnetGatewayIpv4Address = aws.String(ipv4SubNet) + } + + netNSName := "host-daemon" + netNSPath := m.common.GetNetNSPath(netNSName) + netInt, err := networkinterface.New(hostENI, DefaultArg, nil, macToNames) + if err != nil { + logger.Error("Failed to create the network interface", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + + netInt.Default = true + netInt.DesiredStatus = status.NetworkReadyPull + netInt.KnownStatus = status.NetworkNone + daemonNamespace, err := tasknetworkconfig.NewNetworkNamespace(netNSName, netNSPath, + 0, nil, netInt) + if err != nil { + return nil, err + } + daemonNamespace = daemonNamespace.WithNetworkMode("daemon-bridge") + if err != nil { + logger.Error("Error building default network namespace for host mode", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + daemonNamespace.KnownState = status.NetworkNone + daemonNamespace.DesiredState = status.NetworkReadyPull + return []*tasknetworkconfig.NetworkNamespace{daemonNamespace}, nil +} + +// ConfigureDaemonNetNS will create a network namespace using the host ENI and host dns configuration. +// It will contain a loopback interface and a bridge to the internal ECS subnet. +func (m *managedLinux) ConfigureDaemonNetNS(netNS *tasknetworkconfig.NetworkNamespace) error { + ctx := context.Background() + var err error + if netNS.DesiredState == status.NetworkDeleted { + return errors.New("invalid transition state encountered: " + netNS.DesiredState.String()) + } + if netNS.KnownState == status.NetworkNone && + netNS.DesiredState == status.NetworkReadyPull { + + logger.Debug("Creating netns: " + netNS.Path) + // Create network namespace on the host. + err = m.CreateNetNS(netNS.Path) + if err != nil { + return err + } + + logger.Debug("Creating DNS config files") + + // Create necessary DNS config files for the netns. + err = m.CreateDNSConfig(netNS.Path, netNS) + if err != nil { + return err + } + + // Create MI-Bridge + var cniNetConf []ecscni.PluginConfig + cniNetConf = append(cniNetConf, createDaemonBridgePluginConfig(netNS.Path)) + add := true + + _, err = m.common.executeCNIPlugin(ctx, add, cniNetConf...) + if err != nil { + err = errors.Wrap(err, "failed to setup deamon network namespace bridge") + } + + } + + return err +} + +// StopDaemonNetNS stops and cleans up a daemon network namespace. +func (m *managedLinux) StopDaemonNetNS(ctx context.Context, netNS *tasknetworkconfig.NetworkNamespace) error { + + // For now remove the bridge only. + // Deleting the namespace should happen only when we have no more tasks running. + var cniNetConf []ecscni.PluginConfig + cniNetConf = append(cniNetConf, createDaemonBridgePluginConfig(netNS.Path)) + add := false + + _, err := m.common.executeCNIPlugin(ctx, add, cniNetConf...) + if err != nil { + err = errors.Wrap(err, "failed to stop deamon network namespace bridge") + } + + return err +} diff --git a/ecs-agent/netlib/platform/managed_linux_test.go b/ecs-agent/netlib/platform/managed_linux_test.go index fe7984e402e..de8235998ef 100644 --- a/ecs-agent/netlib/platform/managed_linux_test.go +++ b/ecs-agent/netlib/platform/managed_linux_test.go @@ -131,7 +131,7 @@ func testManagedLinuxBranchENIConfiguration(t *testing.T) { require.NoError(t, err) } -func TestBuildDefaultNetworkNamespace(t *testing.T) { +func TestBuildDefaultNetworkNamespaceConfig(t *testing.T) { tests := []struct { name string taskID string @@ -278,7 +278,7 @@ func TestBuildDefaultNetworkNamespace(t *testing.T) { common: *commonPlatform, } - namespaces, err := ml.buildDefaultNetworkNamespace(tt.taskID) + namespaces, err := ml.buildDefaultNetworkNamespaceConfig(tt.taskID) if tt.expectedError != nil { assert.Error(t, err) @@ -322,6 +322,121 @@ func TestBuildDefaultNetworkNamespace(t *testing.T) { } } +func TestBuildHostDaemonNamespaceConfig(t *testing.T) { + tests := []struct { + name string + taskID string + setupMocks func( + *mock_ec2.MockEC2MetadataClient, + *mock_netwrapper.MockNet, + *mock_netlinkwrapper.MockNetLink, + ) + expectedError error + expectedIPAddress string + expectedSubnetGatewayAddress string + expectedNetworkMode string + }{ + { + name: "successful daemon namespace creation", + taskID: "daemon-task-1", + setupMocks: func( + mockEC2Client *mock_ec2.MockEC2MetadataClient, + mockNet *mock_netwrapper.MockNet, + mockNetLink *mock_netlinkwrapper.MockNetLink) { + mockEC2Client.EXPECT().GetMetadata(PrivateIPv4Address).Return("10.194.20.1", nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(MacResource).Return(macAddress, nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(InstanceIDResource).Return("i-1234567890abcdef0", nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(fmt.Sprintf(IPv4SubNetCidrBlock, macAddress)). + Return("10.194.20.0/20", nil). + Times(1) + + testMac, err := net.ParseMAC(macAddress) + require.NoError(t, err) + link1 := &netlink.Dummy{LinkAttrs: netlink.LinkAttrs{HardwareAddr: testMac}} + mockNetLink.EXPECT().LinkList().Return([]netlink.Link{link1}, nil) + routes := []netlink.Route{ + netlink.Route{ + Gw: nil, + Dst: nil, + LinkIndex: 0, + }, + netlink.Route{ + Gw: net.ParseIP("10.194.20.1"), + Dst: nil, + LinkIndex: 0, + }, + } + mockNetLink.EXPECT().RouteList(link1, netlink.FAMILY_V4).Return(routes, nil).Times(1) + mockNetLink.EXPECT().RouteList(link1, netlink.FAMILY_V6).Return(nil, nil).Times(1) + + testIface := []net.Interface{ + { + HardwareAddr: testMac, + Name: "eth1", + }, + } + mockNet.EXPECT().Interfaces().Return(testIface, nil).Times(1) + }, + expectedIPAddress: "10.194.20.1", + expectedSubnetGatewayAddress: "10.194.20.0/20", + expectedNetworkMode: "daemon-bridge", + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) + mockNet := mock_netwrapper.NewMockNet(ctrl) + netLink := mock_netlinkwrapper.NewMockNetLink(ctrl) + mockNsUtil := mock_ecscni.NewMockNetNSUtil(ctrl) + mockNsUtil.EXPECT().GetNetNSPath("host-daemon").Return("/var/run/netns/host-daemon").Times(1) + tt.setupMocks(mockMetadataClient, mockNet, netLink) + + commonPlatform := &common{ + net: mockNet, + netlink: netLink, + nsUtil: mockNsUtil, + } + ml := &managedLinux{ + client: mockMetadataClient, + common: *commonPlatform, + } + + namespaces, err := ml.buildHostDaemonNamespaceConfig(tt.taskID) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError.Error()) + assert.Nil(t, namespaces) + } else { + assert.NoError(t, err) + assert.NotNil(t, namespaces) + assert.Len(t, namespaces, 1) + + ns := namespaces[0] + // Verify namespace properties + assert.Equal(t, "host-daemon", ns.Name) + assert.Equal(t, tt.expectedNetworkMode, string(ns.NetworkMode)) + assert.Equal(t, status.NetworkNone, ns.KnownState) + assert.Equal(t, status.NetworkReadyPull, ns.DesiredState) + + // Verify network interface properties + netInt := ns.NetworkInterfaces[0] + assert.True(t, netInt.Default) + assert.Equal(t, status.NetworkReadyPull, netInt.DesiredStatus) + assert.Equal(t, status.NetworkNone, netInt.KnownStatus) + assert.Equal(t, "i-1234567890abcdef0", netInt.ID) + assert.Equal(t, tt.expectedIPAddress, netInt.IPV4Addresses[0].Address) + assert.Equal(t, tt.expectedSubnetGatewayAddress, netInt.SubnetGatewayIPV4Address) + } + }) + } +} + // setupManagedLinuxTestConfigureInterface provisions all the resources needed to facilitate the two // subtests in TestManagedLinux_TestConfigureInterface. func setupManagedLinuxTestConfigureInterface( diff --git a/ecs-agent/netlib/platform/mocks/platform_mocks.go b/ecs-agent/netlib/platform/mocks/platform_mocks.go index f97a1763f22..34cb901b612 100644 --- a/ecs-agent/netlib/platform/mocks/platform_mocks.go +++ b/ecs-agent/netlib/platform/mocks/platform_mocks.go @@ -83,6 +83,20 @@ func (mr *MockAPIMockRecorder) ConfigureAppMesh(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureAppMesh", reflect.TypeOf((*MockAPI)(nil).ConfigureAppMesh), arg0, arg1, arg2) } +// ConfigureDaemonNetNS mocks base method. +func (m *MockAPI) ConfigureDaemonNetNS(arg0 *tasknetworkconfig.NetworkNamespace) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfigureDaemonNetNS", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// ConfigureDaemonNetNS indicates an expected call of ConfigureDaemonNetNS. +func (mr *MockAPIMockRecorder) ConfigureDaemonNetNS(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureDaemonNetNS", reflect.TypeOf((*MockAPI)(nil).ConfigureDaemonNetNS), arg0) +} + // ConfigureInterface mocks base method. func (m *MockAPI) ConfigureInterface(arg0 context.Context, arg1 string, arg2 *networkinterface.NetworkInterface, arg3 data.NetworkDataClient) error { m.ctrl.T.Helper() @@ -194,3 +208,17 @@ func (mr *MockAPIMockRecorder) HandleHostMode() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleHostMode", reflect.TypeOf((*MockAPI)(nil).HandleHostMode)) } + +// StopDaemonNetNS mocks base method. +func (m *MockAPI) StopDaemonNetNS(arg0 context.Context, arg1 *tasknetworkconfig.NetworkNamespace) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopDaemonNetNS", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopDaemonNetNS indicates an expected call of StopDaemonNetNS. +func (mr *MockAPIMockRecorder) StopDaemonNetNS(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopDaemonNetNS", reflect.TypeOf((*MockAPI)(nil).StopDaemonNetNS), arg0, arg1) +}