From 94e91f3b0534af5fc03218ca54efc73a89fe2993 Mon Sep 17 00:00:00 2001 From: Aviral Khattar Date: Wed, 12 Nov 2025 22:01:13 +0000 Subject: [PATCH] Managed daemons networking changes, to manage daemon-bridge network mode. --- ecs-agent/netlib/common_test.go | 22 +- .../tasknetworkconfig/network_namespace.go | 12 + .../network_namespace_test.go | 40 ++++ ecs-agent/netlib/network_builder.go | 4 + .../netlib/network_builder_linux_test.go | 64 +++++ ecs-agent/netlib/platform/api.go | 8 + ecs-agent/netlib/platform/common.go | 13 + ecs-agent/netlib/platform/common_linux.go | 2 +- .../netlib/platform/containerd_windows.go | 7 +- ecs-agent/netlib/platform/managed_linux.go | 175 +++++++++++++- .../netlib/platform/managed_linux_test.go | 224 +++++++++++++++++- .../netlib/platform/mocks/platform_mocks.go | 28 +++ 12 files changed, 581 insertions(+), 18 deletions(-) diff --git a/ecs-agent/netlib/common_test.go b/ecs-agent/netlib/common_test.go index 73647040b1c..917d3659ade 100644 --- a/ecs-agent/netlib/common_test.go +++ b/ecs-agent/netlib/common_test.go @@ -71,9 +71,10 @@ func getSingleNetNSAWSVPCTestData(testTaskID string) (*ecsacs.Task, tasknetworkc NetworkMode: types.NetworkModeAwsvpc, NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{ { - Name: netNSName, - Path: netNSPath, - Index: 0, + Name: netNSName, + Path: netNSPath, + Index: 0, + NetworkMode: types.NetworkModeAwsvpc, NetworkInterfaces: []*networkinterface.NetworkInterface{ &netIfs[0], }, @@ -156,9 +157,10 @@ func getMultiNetNSMultiIfaceAWSVPCTestData(testTaskID string) (*ecsacs.Task, tas NetworkMode: types.NetworkModeAwsvpc, NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{ { - Name: primaryNetNSName, - Path: primaryNetNSPath, - Index: 0, + Name: primaryNetNSName, + Path: primaryNetNSPath, + Index: 0, + NetworkMode: types.NetworkModeAwsvpc, NetworkInterfaces: []*networkinterface.NetworkInterface{ &netIfs[0], }, @@ -166,9 +168,10 @@ func getMultiNetNSMultiIfaceAWSVPCTestData(testTaskID string) (*ecsacs.Task, tas DesiredState: status.NetworkReadyPull, }, { - Name: secondaryNetNSName, - Path: secondaryNetNSPath, - Index: 1, + Name: secondaryNetNSName, + Path: secondaryNetNSPath, + Index: 1, + NetworkMode: types.NetworkModeAwsvpc, NetworkInterfaces: []*networkinterface.NetworkInterface{ &netIfs[1], }, @@ -323,6 +326,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..aaa7107ad53 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), host(managed-instances only), 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..ecb8c8e0613 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go @@ -19,6 +19,7 @@ package tasknetworkconfig import ( "testing" + "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -47,6 +48,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, types.NetworkModeAwsvpc, 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 +80,41 @@ func TestNetworkNamespace_IsPrimary(t *testing.T) { require.Equal(t, tc.isPrimary, tc.netNS.IsPrimary()) } } + +func TestNetworkNamespace_WithNetworkMode(t *testing.T) { + testCases := []struct { + name string + networkMode types.NetworkMode + }{ + { + name: "awsvpc mode", + networkMode: types.NetworkModeAwsvpc, + }, + { + name: "host mode", + networkMode: types.NetworkModeHost, + }, + { + name: "bridge mode", + networkMode: types.NetworkModeBridge, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + netns := &NetworkNamespace{ + Name: "test-netns", + Path: "/test/path", + Index: 0, + NetworkMode: types.NetworkModeAwsvpc, // default + } + + result := netns.WithNetworkMode(tc.networkMode) + + // Verify the method returns the same instance + assert.Same(t, netns, result) + // Verify the NetworkMode was updated + assert.Equal(t, tc.networkMode, netns.NetworkMode) + }) + } +} diff --git a/ecs-agent/netlib/network_builder.go b/ecs-agent/netlib/network_builder.go index 4783aafa3b3..c53fbd7c53f 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,8 @@ 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": + 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..7ba1b550a22 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,65 @@ 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 for daemon-bridge network mode. +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, + } + + netNS := &tasknetworkconfig.NetworkNamespace{ + Name: "daemon-test", + Path: "/var/run/netns/daemon-test", + } + + 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), + ) + + err := netBuilder.Start(ctx, "daemon-bridge", taskID, netNS) + require.NoError(t, err) +} + +// testNetworkBuilder_StopDaemonBridge verifies that the expected platform API calls +// are made by the network builder for stopping daemon-bridge network mode. +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, + } + + netNS := &tasknetworkconfig.NetworkNamespace{ + Name: "daemon-test", + Path: "/var/run/netns/daemon-test", + } + + 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), + ) + + err := netBuilder.Stop(ctx, "daemon-bridge", taskID, netNS) + require.NoError(t, err) +} 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/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/common_linux.go b/ecs-agent/netlib/platform/common_linux.go index cef197675bb..cf4f1b175c3 100644 --- a/ecs-agent/netlib/platform/common_linux.go +++ b/ecs-agent/netlib/platform/common_linux.go @@ -281,7 +281,7 @@ func (c *common) buildAWSVPCNetworkNamespaces( return netNSs, nil } -// buildNetNS creates a single network namespace object using the input network config data. +// buildNetNS creates a single awsvpc network namespace object using the input network config data. func (c *common) buildNetNS( taskID string, index int, 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..73458e3492a 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.buildHostNetworkNamespaceConfig(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)) } @@ -204,8 +209,8 @@ func (m *managedLinux) ConfigureServiceConnect( return m.common.configureServiceConnect(ctx, netNSPath, primaryIf, scConfig) } -// buildDefaultNetworkNamespace return default network namespace of host ENI for host mode. -func (m *managedLinux) buildDefaultNetworkNamespace(taskID string) ([]*tasknetworkconfig.NetworkNamespace, error) { +// buildHostNetworkNamespaceConfig return default network namespace of host ENI for host mode. +func (m *managedLinux) buildHostNetworkNamespaceConfig(taskID string) ([]*tasknetworkconfig.NetworkNamespace, error) { macAddress, err1 := m.client.GetMetadata(MacResource) ec2ID, err2 := m.client.GetMetadata(InstanceIDResource) macToNames, err3 := m.common.interfacesMACToName() @@ -291,6 +296,7 @@ func (m *managedLinux) buildDefaultNetworkNamespace(taskID string) ([]*tasknetwo netInt.DesiredStatus = status.NetworkReady netInt.KnownStatus = status.NetworkReady defaultNameSpace, err := tasknetworkconfig.NewNetworkNamespace(netNSName, "", 0, nil, netInt) + defaultNameSpace = defaultNameSpace.WithNetworkMode(types.NetworkModeHost) if err != nil { logger.Error("Error building default network namespace for host mode", logger.Fields{ loggerfield.Error: err, @@ -306,3 +312,166 @@ 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) + daemonNamespace = daemonNamespace.WithNetworkMode("daemon-bridge") + if err != nil { + logger.Error("Error building default network namespace for daemon-bridge mode", logger.Fields{ + loggerfield.Error: err, + }) + return nil, err + } + daemonNamespace.KnownState = status.NetworkNone + daemonNamespace.DesiredState = status.NetworkReadyPull + return []*tasknetworkconfig.NetworkNamespace{daemonNamespace}, nil +} + +func (m *managedLinux) configureDaemonNetNS(ctx context.Context, taskID string, netNS *tasknetworkconfig.NetworkNamespace) error { + 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 daemon 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 for daemon NS") + + // Create necessary DNS config files for the netns. + err = m.CreateDNSConfig(taskID, netNS) + if err != nil { + return err + } + + // Create MI-Bridge + var cniNetConf []ecscni.PluginConfig + cniNetConf = append(cniNetConf, createBridgePluginConfig(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 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 { + return m.configureDaemonNetNS(context.Background(), netNS.Path, netNS) +} + +// StopDaemonNetNS stops and cleans up a daemon network namespace. +func (m *managedLinux) StopDaemonNetNS(ctx context.Context, netNS *tasknetworkconfig.NetworkNamespace) error { + + // Cleanup bridge config(veth pair). + var cniNetConf []ecscni.PluginConfig + cniNetConf = append(cniNetConf, createBridgePluginConfig(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") + } + + // TODO : Delete the daemon namespace only when we have no more daemon tasks running. + + return err +} diff --git a/ecs-agent/netlib/platform/managed_linux_test.go b/ecs-agent/netlib/platform/managed_linux_test.go index fe7984e402e..25719279c7a 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.buildHostNetworkNamespaceConfig(tt.taskID) if tt.expectedError != nil { assert.Error(t, err) @@ -484,3 +484,223 @@ func TestManagedLinux_CreateDNSConfig(t *testing.T) { require.NoError(t, err) }) } + +func TestBuildHostDaemonNamespaceConfig(t *testing.T) { + tests := []struct { + name string + taskID string + setupMocks func(*mock_ec2.MockEC2MetadataClient, *mock_netwrapper.MockNet, *mock_netlinkwrapper.MockNetLink, *mock_ecscni.MockNetNSUtil) + expectErr bool + validate func(*testing.T, []*tasknetworkconfig.NetworkNamespace) + }{ + { + name: "successful daemon namespace creation", + taskID: "test-daemon-task", + setupMocks: func(mockEC2Client *mock_ec2.MockEC2MetadataClient, mockNet *mock_netwrapper.MockNet, mockNetLink *mock_netlinkwrapper.MockNetLink, mockNSUtil *mock_ecscni.MockNetNSUtil) { + // First calls for buildHostDaemonNamespaceConfig + mockEC2Client.EXPECT().GetMetadata(MacResource).Return(macAddress, nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(InstanceIDResource).Return("i-1234567890abcdef0", nil).Times(1) + + testMac, _ := net.ParseMAC(macAddress) + testIface := []net.Interface{{HardwareAddr: testMac, Name: "eth0"}} + mockNet.EXPECT().Interfaces().Return(testIface, nil).Times(1) + + // Calls for DetermineIPCompatibility -> FindLinkByMac -> HasDefaultRoute + link1 := &netlink.Dummy{LinkAttrs: netlink.LinkAttrs{HardwareAddr: testMac}} + mockNetLink.EXPECT().LinkList().Return([]netlink.Link{link1}, nil).Times(1) + + // IPv4 and IPv6 route checks + routes := []netlink.Route{ + {Gw: net.ParseIP("10.194.20.1"), Dst: nil}, // default route + } + mockNetLink.EXPECT().RouteList(link1, netlink.FAMILY_V4).Return(routes, nil).Times(1) + mockNetLink.EXPECT().RouteList(link1, netlink.FAMILY_V6).Return(nil, nil).Times(1) + + // IPv4 metadata calls (since IPv4 is compatible) + mockEC2Client.EXPECT().GetMetadata(PrivateIPv4Address).Return("10.194.20.1", nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(fmt.Sprintf(IPv4SubNetCidrBlock, macAddress)).Return("10.194.20.0/20", nil).Times(1) + + // GetNetNSPath call + mockNSUtil.EXPECT().GetNetNSPath("host-daemon").Return("/var/run/netns/host-daemon").Times(1) + }, + expectErr: false, + validate: func(t *testing.T, namespaces []*tasknetworkconfig.NetworkNamespace) { + require.Len(t, namespaces, 1) + ns := namespaces[0] + assert.Equal(t, "daemon-bridge", string(ns.NetworkMode)) + assert.Equal(t, "/var/run/netns/host-daemon", ns.Path) + assert.Equal(t, status.NetworkNone, ns.KnownState) + assert.Equal(t, status.NetworkReadyPull, ns.DesiredState) + + // Verify network interface + require.Len(t, ns.NetworkInterfaces, 1) + netInt := ns.NetworkInterfaces[0] + assert.Equal(t, "i-1234567890abcdef0", netInt.ID) + assert.True(t, netInt.Default) + assert.Equal(t, status.NetworkReadyPull, netInt.DesiredStatus) + assert.Equal(t, status.NetworkNone, netInt.KnownStatus) + }, + }, + { + name: "metadata client error", + taskID: "test-daemon-task", + setupMocks: func(mockEC2Client *mock_ec2.MockEC2MetadataClient, mockNet *mock_netwrapper.MockNet, mockNetLink *mock_netlinkwrapper.MockNetLink, mockNSUtil *mock_ecscni.MockNetNSUtil) { + mockEC2Client.EXPECT().GetMetadata(MacResource).Return("", fmt.Errorf("metadata unavailable")).Times(1) + mockEC2Client.EXPECT().GetMetadata(InstanceIDResource).Return("", fmt.Errorf("metadata unavailable")).Times(1) + + testMac, _ := net.ParseMAC(macAddress) + testIface := []net.Interface{{HardwareAddr: testMac, Name: "eth0"}} + mockNet.EXPECT().Interfaces().Return(testIface, nil).Times(1) + }, + expectErr: true, + validate: nil, + }, + { + name: "interface list error", + taskID: "test-daemon-task", + setupMocks: func(mockEC2Client *mock_ec2.MockEC2MetadataClient, mockNet *mock_netwrapper.MockNet, mockNetLink *mock_netlinkwrapper.MockNetLink, mockNSUtil *mock_ecscni.MockNetNSUtil) { + mockEC2Client.EXPECT().GetMetadata(MacResource).Return(macAddress, nil).Times(1) + mockEC2Client.EXPECT().GetMetadata(InstanceIDResource).Return("i-1234567890abcdef0", nil).Times(1) + mockNet.EXPECT().Interfaces().Return(nil, fmt.Errorf("interface list failed")).Times(1) + }, + expectErr: true, + validate: 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) + tt.setupMocks(mockMetadataClient, mockNet, netLink, mockNSUtil) + + commonPlatform := &common{ + net: mockNet, + netlink: netLink, + nsUtil: mockNSUtil, + } + ml := &managedLinux{ + client: mockMetadataClient, + common: *commonPlatform, + } + + namespaces, err := ml.buildHostDaemonNamespaceConfig(tt.taskID) + + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, namespaces) + } else { + assert.NoError(t, err) + assert.NotNil(t, namespaces) + if tt.validate != nil { + tt.validate(t, namespaces) + } + } + }) + } +} + +func TestConfigureDaemonNetNS(t *testing.T) { + tests := []struct { + name string + netNS *tasknetworkconfig.NetworkNamespace + expectErr bool + }{ + { + name: "invalid transition state", + netNS: &tasknetworkconfig.NetworkNamespace{ + Path: "/var/run/netns/test-daemon", + NetworkMode: "daemon-bridge", + KnownState: status.NetworkNone, + DesiredState: status.NetworkDeleted, + }, + expectErr: true, + }, + { + name: "wrong state transition", + netNS: &tasknetworkconfig.NetworkNamespace{ + Path: "/var/run/netns/test-daemon", + NetworkMode: "daemon-bridge", + KnownState: status.NetworkReady, + DesiredState: status.NetworkReadyPull, + }, + expectErr: false, // Should return nil without doing anything + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ml := &managedLinux{ + common: common{}, + } + + err := ml.ConfigureDaemonNetNS(tt.netNS) + if tt.expectErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid transition state") + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestStopDaemonNetNS(t *testing.T) { + tests := []struct { + name string + netNS *tasknetworkconfig.NetworkNamespace + setupMock func(*mock_ecscni2.MockCNI) + expectErr bool + }{ + { + name: "successful cleanup", + netNS: &tasknetworkconfig.NetworkNamespace{ + Path: "/var/run/netns/test-daemon", + NetworkMode: "daemon-bridge", + }, + setupMock: func(mockCNI *mock_ecscni2.MockCNI) { + mockCNI.EXPECT().Del(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "CNI delete failure", + netNS: &tasknetworkconfig.NetworkNamespace{ + Path: "/var/run/netns/test-daemon", + NetworkMode: "daemon-bridge", + }, + setupMock: func(mockCNI *mock_ecscni2.MockCNI) { + mockCNI.EXPECT().Del(gomock.Any(), gomock.Any()).Return(fmt.Errorf("CNI delete failed")).Times(1) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCNI := mock_ecscni2.NewMockCNI(ctrl) + tt.setupMock(mockCNI) + + ml := &managedLinux{ + common: common{ + cniClient: mockCNI, + }, + } + + err := ml.StopDaemonNetNS(context.Background(), tt.netNS) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} 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) +}