From b8d2a7f0a61be3315f8d62243c299ff7433a608a Mon Sep 17 00:00:00 2001 From: Samuel J Konat <88858567+samjkon@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:18:14 -0700 Subject: [PATCH] Add network builder and platform APIs (#3939) Added the network builder and platform APIs into the shared library package. Network builder is intended to act as the API to be consumed by the agent to setup networking resources on the host. Network builder invokes the platform APIs to execute platform specific operations like creation of network namespaces etc. --------- Co-authored-by: Samuel Konat --- agent/acs/session/payload_responder.go | 2 +- .../networkinterface/networkinterface.go | 68 ++-- .../networkinterface_status.go | 42 -- .../netlib/model/status/network_status.go | 42 ++ agent/vendor/modules.txt | 1 + ecs-agent/netlib/common_test.go | 23 ++ .../networkinterface/networkinterface.go | 68 ++-- .../networkinterface_status.go | 42 -- .../networkinterface/networkinterface_test.go | 373 ------------------ .../model/tasknetworkconfig/common_test.go | 11 +- .../tasknetworkconfig/network_namespace.go | 34 +- .../network_namespace_test.go | 32 +- .../tasknetworkconfig/task_network_config.go | 21 +- .../task_network_config_test.go | 40 ++ ecs-agent/netlib/network_builder.go | 46 +++ .../netlib/network_builder_linux_test.go | 279 +++++++++++++ ecs-agent/netlib/platform/api.go | 33 ++ ecs-agent/netlib/platform/common_linux.go | 207 ++++++++++ .../netlib/platform/common_linux_test.go | 18 + ecs-agent/netlib/platform/containerd_linux.go | 31 ++ .../netlib/platform/containerd_windows.go | 42 ++ 21 files changed, 913 insertions(+), 542 deletions(-) delete mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface_status.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status/network_status.go create mode 100644 ecs-agent/netlib/common_test.go delete mode 100644 ecs-agent/netlib/model/networkinterface/networkinterface_status.go delete mode 100644 ecs-agent/netlib/model/networkinterface/networkinterface_test.go create mode 100644 ecs-agent/netlib/network_builder.go create mode 100644 ecs-agent/netlib/network_builder_linux_test.go create mode 100644 ecs-agent/netlib/platform/api.go create mode 100644 ecs-agent/netlib/platform/common_linux.go create mode 100644 ecs-agent/netlib/platform/common_linux_test.go create mode 100644 ecs-agent/netlib/platform/containerd_linux.go create mode 100644 ecs-agent/netlib/platform/containerd_windows.go diff --git a/agent/acs/session/payload_responder.go b/agent/acs/session/payload_responder.go index 770de32667..4656c5f3af 100644 --- a/agent/acs/session/payload_responder.go +++ b/agent/acs/session/payload_responder.go @@ -140,7 +140,7 @@ func (pmHandler *payloadMessageHandler) addPayloadTasks(payload *ecsacs.PayloadM // Add ENI information to the task struct. for _, acsENI := range task.ElasticNetworkInterfaces { - eni, err := ni.ENIFromACS(acsENI) + eni, err := ni.InterfaceFromACS(acsENI) if err != nil { pmHandler.handleInvalidTask(task, err, payload) allTasksOK = false diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface.go index 72667e7d03..3ed4a1da31 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface.go @@ -24,6 +24,8 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" loggerfield "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status" + "github.com/aws/aws-sdk-go/aws" "github.com/pkg/errors" ) @@ -53,15 +55,13 @@ type NetworkInterface struct { // InterfaceAssociationProtocol is the type of NetworkInterface, valid value: "default", "vlan" InterfaceAssociationProtocol string `json:",omitempty"` - Index int64 `json:"Index,omitempty"` - UserID uint32 `json:"UserID,omitempty"` - Name string `json:"Name,omitempty"` - NetNSName string `json:"NetNSName,omitempty"` - NetNSPath string `json:"NetNSPath,omitempty"` - DeviceName string `json:"DeviceName,omitempty"` - GuestNetNSName string `json:"GuestNetNSName,omitempty"` - KnownStatus Status `json:"KnownStatus,omitempty"` - DesiredStatus Status `json:"DesiredStatus,omitempty"` + Index int64 `json:"Index,omitempty"` + UserID uint32 `json:"UserID,omitempty"` + Name string `json:"Name,omitempty"` + DeviceName string `json:"DeviceName,omitempty"` + GuestNetNSName string `json:"GuestNetNSName,omitempty"` + KnownStatus status.NetworkStatus `json:"KnownStatus,omitempty"` + DesiredStatus status.NetworkStatus `json:"DesiredStatus,omitempty"` // InterfaceVlanProperties contains information for an interface // that is supposed to be used as a VLAN device @@ -84,6 +84,10 @@ type NetworkInterface struct { ipv4SubnetCIDRBlock string ipv6SubnetCIDRBlock string + // Default denotes whether the interface is responsible + // for handling default route within the netns it resides in. + Default bool + // guard protects access to fields of this struct. guard sync.RWMutex } @@ -368,8 +372,8 @@ type IPV6Address struct { Address string } -// ENIFromACS validates the given ACS NetworkInterface information and creates an NetworkInterface object from it. -func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, error) { +// InterfaceFromACS validates the given ACS NetworkInterface information and creates an NetworkInterface object from it. +func InterfaceFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, error) { err := ValidateENI(acsENI) if err != nil { return nil, err @@ -393,14 +397,6 @@ func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, erro }) } - // Read NetworkInterface association properties. - var interfaceVlanProperties InterfaceVlanProperties - - if aws.StringValue(acsENI.InterfaceAssociationProtocol) == VLANInterfaceAssociationProtocol { - interfaceVlanProperties.TrunkInterfaceMacAddress = aws.StringValue(acsENI.InterfaceVlanProperties.TrunkInterfaceMacAddress) - interfaceVlanProperties.VlanID = aws.StringValue(acsENI.InterfaceVlanProperties.VlanId) - } - ni := &NetworkInterface{ ID: aws.StringValue(acsENI.Ec2Id), MacAddress: aws.StringValue(acsENI.MacAddress), @@ -409,7 +405,14 @@ func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, erro SubnetGatewayIPV4Address: aws.StringValue(acsENI.SubnetGatewayIpv4Address), PrivateDNSName: aws.StringValue(acsENI.PrivateDnsName), InterfaceAssociationProtocol: aws.StringValue(acsENI.InterfaceAssociationProtocol), - InterfaceVlanProperties: &interfaceVlanProperties, + } + + // Read NetworkInterface association properties. + if aws.StringValue(acsENI.InterfaceAssociationProtocol) == VLANInterfaceAssociationProtocol { + var interfaceVlanProperties InterfaceVlanProperties + interfaceVlanProperties.TrunkInterfaceMacAddress = aws.StringValue(acsENI.InterfaceVlanProperties.TrunkInterfaceMacAddress) + interfaceVlanProperties.VlanID = aws.StringValue(acsENI.InterfaceVlanProperties.VlanId) + ni.InterfaceVlanProperties = &interfaceVlanProperties } for _, nameserverIP := range acsENI.DomainNameServers { @@ -470,8 +473,6 @@ func ValidateENI(acsENI *ecsacs.ElasticNetworkInterface) error { // New creates a new NetworkInterface model. func New( acsENI *ecsacs.ElasticNetworkInterface, - netNSName string, - netNSPath string, guestNetNSName string, peerInterface *ecsacs.ElasticNetworkInterface, ) (*NetworkInterface, error) { @@ -501,9 +502,9 @@ func New( // by the common NetworkInterface handler. default: // Acquire the NetworkInterface information from the payload. - networkInterface, err = ENIFromACS(acsENI) + networkInterface, err = InterfaceFromACS(acsENI) if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal eni") + return nil, errors.Wrap(err, "failed to unmarshal interface model") } // Historically, if there is no interface association protocol in the NetworkInterface payload, we assume @@ -514,11 +515,9 @@ func New( } networkInterface.Index = aws.Int64Value(acsENI.Index) - networkInterface.Name = GetENIName(acsENI) - networkInterface.KnownStatus = StatusNone - networkInterface.DesiredStatus = StatusReadyPull - networkInterface.NetNSName = netNSName - networkInterface.NetNSPath = netNSPath + networkInterface.Name = GetInterfaceName(acsENI) + networkInterface.KnownStatus = status.NetworkNone + networkInterface.DesiredStatus = status.NetworkReadyPull networkInterface.GuestNetNSName = guestNetNSName return networkInterface, nil @@ -569,11 +568,11 @@ func (ni *NetworkInterface) IsPrimary() bool { // it was decided that for firecracker platform the files had to be generated for secondary ENIs as well. // Hence the NetworkInterface IsPrimary check was moved from here to warmpool specific APIs. func (ni *NetworkInterface) ShouldGenerateNetworkConfigFiles() bool { - return ni.DesiredStatus == StatusReadyPull + return ni.DesiredStatus == status.NetworkReadyPull } -// GetENIName creates the NetworkInterface name from the NetworkInterface mac address in case it is empty in the ACS payload. -func GetENIName(acsENI *ecsacs.ElasticNetworkInterface) string { +// GetInterfaceName creates the NetworkInterface name from the NetworkInterface mac address in case it is empty in the ACS payload. +func GetInterfaceName(acsENI *ecsacs.ElasticNetworkInterface) string { if acsENI.Name != nil { return aws.StringValue(acsENI.Name) } @@ -645,3 +644,8 @@ func vethPairFromACS( }, nil } + +// NetNSName returns the netns name that the specified network interface will be attached to in a desired task. +func NetNSName(taskID, eniName string) string { + return fmt.Sprintf("%s-%s", taskID, eniName) +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface_status.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface_status.go deleted file mode 100644 index 6b10dde38e..0000000000 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface/networkinterface_status.go +++ /dev/null @@ -1,42 +0,0 @@ -package networkinterface - -// Status represents the status of an ENI resource. -type Status string - -const ( - // StatusNone is the initial staus of the ENI. - StatusNone Status = "NONE" - // StatusReadyPull indicates that the ENI is ready for downloading resources associated with - // the execution role. This includes container images, task secrets and configs. - StatusReadyPull Status = "READY_PULL" - // StatusReady indicates that the ENI is ready for use by containers in the task. - StatusReady Status = "READY" - // StatusDeleted indicates that the ENI is deleted. - StatusDeleted Status = "DELETED" -) - -var ( - eniStatusOrder = map[Status]int{ - StatusNone: 0, - StatusReadyPull: 1, - StatusReady: 2, - StatusDeleted: 3, - } -) - -func (es Status) String() string { - return string(es) -} - -func (es Status) StatusBackwards(es2 Status) bool { - return eniStatusOrder[es] < eniStatusOrder[es2] -} - -func GetAllStatuses() []Status { - return []Status{ - StatusNone, - StatusReadyPull, - StatusReady, - StatusDeleted, - } -} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status/network_status.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status/network_status.go new file mode 100644 index 0000000000..b6fdb9d2d8 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status/network_status.go @@ -0,0 +1,42 @@ +package status + +// NetworkStatus represents the status of a network resource. +type NetworkStatus string + +const ( + // NetworkNone is the initial status of the ENI. + NetworkNone NetworkStatus = "NONE" + // NetworkReadyPull indicates that the ENI is ready for downloading resources associated with + // the execution role. This includes container images, task secrets and configs. + NetworkReadyPull NetworkStatus = "READY_PULL" + // NetworkReady indicates that the ENI is ready for use by containers in the task. + NetworkReady NetworkStatus = "READY" + // NetworkDeleted indicates that the ENI is deleted. + NetworkDeleted NetworkStatus = "DELETED" +) + +var ( + eniStatusOrder = map[NetworkStatus]int{ + NetworkNone: 0, + NetworkReadyPull: 1, + NetworkReady: 2, + NetworkDeleted: 3, + } +) + +func (es NetworkStatus) String() string { + return string(es) +} + +func (es NetworkStatus) ENIStatusBackwards(es2 NetworkStatus) bool { + return eniStatusOrder[es] < eniStatusOrder[es2] +} + +func GetAllENIStatuses() []NetworkStatus { + return []NetworkStatus{ + NetworkNone, + NetworkReadyPull, + NetworkReady, + NetworkDeleted, + } +} diff --git a/agent/vendor/modules.txt b/agent/vendor/modules.txt index bfc197d7bd..23f70f1d14 100644 --- a/agent/vendor/modules.txt +++ b/agent/vendor/modules.txt @@ -44,6 +44,7 @@ github.com/aws/amazon-ecs-agent/ecs-agent/metrics github.com/aws/amazon-ecs-agent/ecs-agent/modeltransformer github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/appmesh github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface +github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status github.com/aws/amazon-ecs-agent/ecs-agent/stats github.com/aws/amazon-ecs-agent/ecs-agent/tcs/client github.com/aws/amazon-ecs-agent/ecs-agent/tcs/handler diff --git a/ecs-agent/netlib/common_test.go b/ecs-agent/netlib/common_test.go new file mode 100644 index 0000000000..df29b87318 --- /dev/null +++ b/ecs-agent/netlib/common_test.go @@ -0,0 +1,23 @@ +package netlib + +const ( + taskID = "random-task-id" + eniMAC = "f0:5c:89:a3:ab:01" + eniName = "f05c89a3ab01" + eniMAC2 = "f0:5c:89:a3:ab:02" + eniName2 = "f05c89a3ab02" + eniID = "eni-abdf1234" + eniID2 = "eni-abdf12342" + dnsName = "amazon.com" + nameServer = "10.1.0.2" + nameServer2 = "10.2.0.2" + ipv4Addr = "10.1.0.196" + ipv4Addr2 = "10.2.0.196" + ipv6Addr = "2600:1f13:4d9:e611:9009:ac97:1ab4:17d1" + ipv6Addr2 = "2600:1f13:4d9:e611:9009:ac97:1ab4:17d2" + subnetGatewayCIDR = "10.1.0.1/24" + subnetGatewayCIDR2 = "10.2.0.1/24" + netNSNamePattern = "%s-%s" + searchDomainName = "us-west-2.test.compute.internal" + netNSPathDir = "/var/run/netns/" +) diff --git a/ecs-agent/netlib/model/networkinterface/networkinterface.go b/ecs-agent/netlib/model/networkinterface/networkinterface.go index 72667e7d03..3ed4a1da31 100644 --- a/ecs-agent/netlib/model/networkinterface/networkinterface.go +++ b/ecs-agent/netlib/model/networkinterface/networkinterface.go @@ -24,6 +24,8 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" loggerfield "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status" + "github.com/aws/aws-sdk-go/aws" "github.com/pkg/errors" ) @@ -53,15 +55,13 @@ type NetworkInterface struct { // InterfaceAssociationProtocol is the type of NetworkInterface, valid value: "default", "vlan" InterfaceAssociationProtocol string `json:",omitempty"` - Index int64 `json:"Index,omitempty"` - UserID uint32 `json:"UserID,omitempty"` - Name string `json:"Name,omitempty"` - NetNSName string `json:"NetNSName,omitempty"` - NetNSPath string `json:"NetNSPath,omitempty"` - DeviceName string `json:"DeviceName,omitempty"` - GuestNetNSName string `json:"GuestNetNSName,omitempty"` - KnownStatus Status `json:"KnownStatus,omitempty"` - DesiredStatus Status `json:"DesiredStatus,omitempty"` + Index int64 `json:"Index,omitempty"` + UserID uint32 `json:"UserID,omitempty"` + Name string `json:"Name,omitempty"` + DeviceName string `json:"DeviceName,omitempty"` + GuestNetNSName string `json:"GuestNetNSName,omitempty"` + KnownStatus status.NetworkStatus `json:"KnownStatus,omitempty"` + DesiredStatus status.NetworkStatus `json:"DesiredStatus,omitempty"` // InterfaceVlanProperties contains information for an interface // that is supposed to be used as a VLAN device @@ -84,6 +84,10 @@ type NetworkInterface struct { ipv4SubnetCIDRBlock string ipv6SubnetCIDRBlock string + // Default denotes whether the interface is responsible + // for handling default route within the netns it resides in. + Default bool + // guard protects access to fields of this struct. guard sync.RWMutex } @@ -368,8 +372,8 @@ type IPV6Address struct { Address string } -// ENIFromACS validates the given ACS NetworkInterface information and creates an NetworkInterface object from it. -func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, error) { +// InterfaceFromACS validates the given ACS NetworkInterface information and creates an NetworkInterface object from it. +func InterfaceFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, error) { err := ValidateENI(acsENI) if err != nil { return nil, err @@ -393,14 +397,6 @@ func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, erro }) } - // Read NetworkInterface association properties. - var interfaceVlanProperties InterfaceVlanProperties - - if aws.StringValue(acsENI.InterfaceAssociationProtocol) == VLANInterfaceAssociationProtocol { - interfaceVlanProperties.TrunkInterfaceMacAddress = aws.StringValue(acsENI.InterfaceVlanProperties.TrunkInterfaceMacAddress) - interfaceVlanProperties.VlanID = aws.StringValue(acsENI.InterfaceVlanProperties.VlanId) - } - ni := &NetworkInterface{ ID: aws.StringValue(acsENI.Ec2Id), MacAddress: aws.StringValue(acsENI.MacAddress), @@ -409,7 +405,14 @@ func ENIFromACS(acsENI *ecsacs.ElasticNetworkInterface) (*NetworkInterface, erro SubnetGatewayIPV4Address: aws.StringValue(acsENI.SubnetGatewayIpv4Address), PrivateDNSName: aws.StringValue(acsENI.PrivateDnsName), InterfaceAssociationProtocol: aws.StringValue(acsENI.InterfaceAssociationProtocol), - InterfaceVlanProperties: &interfaceVlanProperties, + } + + // Read NetworkInterface association properties. + if aws.StringValue(acsENI.InterfaceAssociationProtocol) == VLANInterfaceAssociationProtocol { + var interfaceVlanProperties InterfaceVlanProperties + interfaceVlanProperties.TrunkInterfaceMacAddress = aws.StringValue(acsENI.InterfaceVlanProperties.TrunkInterfaceMacAddress) + interfaceVlanProperties.VlanID = aws.StringValue(acsENI.InterfaceVlanProperties.VlanId) + ni.InterfaceVlanProperties = &interfaceVlanProperties } for _, nameserverIP := range acsENI.DomainNameServers { @@ -470,8 +473,6 @@ func ValidateENI(acsENI *ecsacs.ElasticNetworkInterface) error { // New creates a new NetworkInterface model. func New( acsENI *ecsacs.ElasticNetworkInterface, - netNSName string, - netNSPath string, guestNetNSName string, peerInterface *ecsacs.ElasticNetworkInterface, ) (*NetworkInterface, error) { @@ -501,9 +502,9 @@ func New( // by the common NetworkInterface handler. default: // Acquire the NetworkInterface information from the payload. - networkInterface, err = ENIFromACS(acsENI) + networkInterface, err = InterfaceFromACS(acsENI) if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal eni") + return nil, errors.Wrap(err, "failed to unmarshal interface model") } // Historically, if there is no interface association protocol in the NetworkInterface payload, we assume @@ -514,11 +515,9 @@ func New( } networkInterface.Index = aws.Int64Value(acsENI.Index) - networkInterface.Name = GetENIName(acsENI) - networkInterface.KnownStatus = StatusNone - networkInterface.DesiredStatus = StatusReadyPull - networkInterface.NetNSName = netNSName - networkInterface.NetNSPath = netNSPath + networkInterface.Name = GetInterfaceName(acsENI) + networkInterface.KnownStatus = status.NetworkNone + networkInterface.DesiredStatus = status.NetworkReadyPull networkInterface.GuestNetNSName = guestNetNSName return networkInterface, nil @@ -569,11 +568,11 @@ func (ni *NetworkInterface) IsPrimary() bool { // it was decided that for firecracker platform the files had to be generated for secondary ENIs as well. // Hence the NetworkInterface IsPrimary check was moved from here to warmpool specific APIs. func (ni *NetworkInterface) ShouldGenerateNetworkConfigFiles() bool { - return ni.DesiredStatus == StatusReadyPull + return ni.DesiredStatus == status.NetworkReadyPull } -// GetENIName creates the NetworkInterface name from the NetworkInterface mac address in case it is empty in the ACS payload. -func GetENIName(acsENI *ecsacs.ElasticNetworkInterface) string { +// GetInterfaceName creates the NetworkInterface name from the NetworkInterface mac address in case it is empty in the ACS payload. +func GetInterfaceName(acsENI *ecsacs.ElasticNetworkInterface) string { if acsENI.Name != nil { return aws.StringValue(acsENI.Name) } @@ -645,3 +644,8 @@ func vethPairFromACS( }, nil } + +// NetNSName returns the netns name that the specified network interface will be attached to in a desired task. +func NetNSName(taskID, eniName string) string { + return fmt.Sprintf("%s-%s", taskID, eniName) +} diff --git a/ecs-agent/netlib/model/networkinterface/networkinterface_status.go b/ecs-agent/netlib/model/networkinterface/networkinterface_status.go deleted file mode 100644 index 6b10dde38e..0000000000 --- a/ecs-agent/netlib/model/networkinterface/networkinterface_status.go +++ /dev/null @@ -1,42 +0,0 @@ -package networkinterface - -// Status represents the status of an ENI resource. -type Status string - -const ( - // StatusNone is the initial staus of the ENI. - StatusNone Status = "NONE" - // StatusReadyPull indicates that the ENI is ready for downloading resources associated with - // the execution role. This includes container images, task secrets and configs. - StatusReadyPull Status = "READY_PULL" - // StatusReady indicates that the ENI is ready for use by containers in the task. - StatusReady Status = "READY" - // StatusDeleted indicates that the ENI is deleted. - StatusDeleted Status = "DELETED" -) - -var ( - eniStatusOrder = map[Status]int{ - StatusNone: 0, - StatusReadyPull: 1, - StatusReady: 2, - StatusDeleted: 3, - } -) - -func (es Status) String() string { - return string(es) -} - -func (es Status) StatusBackwards(es2 Status) bool { - return eniStatusOrder[es] < eniStatusOrder[es2] -} - -func GetAllStatuses() []Status { - return []Status{ - StatusNone, - StatusReadyPull, - StatusReady, - StatusDeleted, - } -} diff --git a/ecs-agent/netlib/model/networkinterface/networkinterface_test.go b/ecs-agent/netlib/model/networkinterface/networkinterface_test.go deleted file mode 100644 index 241d586119..0000000000 --- a/ecs-agent/netlib/model/networkinterface/networkinterface_test.go +++ /dev/null @@ -1,373 +0,0 @@ -//go:build unit -// +build unit - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package networkinterface - -import ( - "net" - "testing" - - "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" - "github.com/aws/aws-sdk-go/aws" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - defaultDNS = "169.254.169.253" - customDNS = "10.0.0.2" - customSearchDomain = "us-west-2.compute.internal" - - linkName = "eth1" - macAddr = "02:22:ea:8c:81:dc" - ipv4Addr = "1.2.3.4" - ipv4Gw = "1.2.3.1" - ipv4SubnetPrefixLength = "20" - ipv4Subnet = "1.2.0.0" - ipv4AddrWithPrefixLength = ipv4Addr + "/" + ipv4SubnetPrefixLength - ipv4GwWithPrefixLength = ipv4Gw + "/" + ipv4SubnetPrefixLength - ipv4SubnetCIDRBlock = ipv4Subnet + "/" + ipv4SubnetPrefixLength - ipv6Addr = "abcd:dcba:1234:4321::" - ipv6SubnetPrefixLength = "64" - ipv6SubnetCIDRBlock = ipv6Addr + "/" + ipv6SubnetPrefixLength - ipv6AddrWithPrefixLength = ipv6Addr + "/" + ipv6SubnetPrefixLength - vethPeerInterfaceName = "veth1-peer" - v2nVNI = "ABCDE" - v2nDestinationIP = "10.0.2.129" - v2nDnsIP = "10.3.0.2" - v2nDnsSearch = "us-west-2.test.compute.internal" -) - -var ( - testENI = &NetworkInterface{ - ID: "eni-123", - InterfaceAssociationProtocol: DefaultInterfaceAssociationProtocol, - IPV4Addresses: []*IPV4Address{ - { - Primary: true, - Address: ipv4Addr, - }, - }, - IPV6Addresses: []*IPV6Address{ - { - Address: ipv6Addr, - }, - }, - SubnetGatewayIPV4Address: ipv4GwWithPrefixLength, - } - // validNetInterfacesFunc represents a mock of valid response from net.Interfaces() method. - validNetInterfacesFunc = func() ([]net.Interface, error) { - parsedMAC, _ := net.ParseMAC(macAddr) - return []net.Interface{ - net.Interface{ - Name: linkName, - HardwareAddr: parsedMAC, - }, - }, nil - } - // invalidNetInterfacesFunc represents a mock of error response from net.Interfaces() method. - invalidNetInterfacesFunc = func() ([]net.Interface, error) { - return nil, errors.New("failed to find interfaces") - } -) - -func TestIsStandardENI(t *testing.T) { - testCases := []struct { - protocol string - isStandard bool - }{ - { - protocol: "", - isStandard: true, - }, - { - protocol: DefaultInterfaceAssociationProtocol, - isStandard: true, - }, - { - protocol: VLANInterfaceAssociationProtocol, - isStandard: false, - }, - { - protocol: "invalid", - isStandard: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.protocol, func(t *testing.T) { - ni := &NetworkInterface{ - InterfaceAssociationProtocol: tc.protocol, - } - assert.Equal(t, tc.isStandard, ni.IsStandardENI()) - }) - } -} - -func TestGetIPV4Addresses(t *testing.T) { - assert.Equal(t, []string{ipv4Addr}, testENI.GetIPV4Addresses()) -} - -func TestGetIPV6Addresses(t *testing.T) { - assert.Equal(t, []string{ipv6Addr}, testENI.GetIPV6Addresses()) -} - -func TestGetPrimaryIPv4Address(t *testing.T) { - assert.Equal(t, ipv4Addr, testENI.GetPrimaryIPv4Address()) -} - -func TestGetPrimaryIPv4AddressWithPrefixLength(t *testing.T) { - assert.Equal(t, ipv4AddrWithPrefixLength, testENI.GetPrimaryIPv4AddressWithPrefixLength()) -} - -func TestGetIPAddressesWithPrefixLength(t *testing.T) { - assert.Equal(t, []string{ipv4AddrWithPrefixLength, ipv6AddrWithPrefixLength}, testENI.GetIPAddressesWithPrefixLength()) -} - -func TestGetIPv4SubnetPrefixLength(t *testing.T) { - assert.Equal(t, ipv4SubnetPrefixLength, testENI.GetIPv4SubnetPrefixLength()) -} - -func TestGetIPv4SubnetCIDRBlock(t *testing.T) { - assert.Equal(t, ipv4SubnetCIDRBlock, testENI.GetIPv4SubnetCIDRBlock()) -} - -func TestGetIPv6SubnetCIDRBlock(t *testing.T) { - assert.Equal(t, ipv6SubnetCIDRBlock, testENI.GetIPv6SubnetCIDRBlock()) -} - -func TestGetSubnetGatewayIPv4Address(t *testing.T) { - assert.Equal(t, ipv4Gw, testENI.GetSubnetGatewayIPv4Address()) -} - -// TestGetLinkNameSuccess tests the retrieval of ENIs name on the instance. -func TestGetLinkNameSuccess(t *testing.T) { - netInterfaces = validNetInterfacesFunc - ni := &NetworkInterface{ - MacAddress: macAddr, - } - - eniLinkName := ni.GetLinkName() - assert.EqualValues(t, linkName, eniLinkName) -} - -// TestGetLinkNameFailure tests the retrieval of Network Interface Name in case of failure. -func TestGetLinkNameFailure(t *testing.T) { - netInterfaces = invalidNetInterfacesFunc - ni := &NetworkInterface{ - MacAddress: macAddr, - } - - eniLinkName := ni.GetLinkName() - assert.EqualValues(t, "", eniLinkName) -} - -func TestENIToString(t *testing.T) { - expectedStr := `eni id:eni-123, mac: , hostname: , ipv4addresses: [1.2.3.4], ipv6addresses: [abcd:dcba:1234:4321::], dns: [], dns search: [], gateway ipv4: [1.2.3.1/20][]` - assert.Equal(t, expectedStr, testENI.String()) -} - -// TestENIFromACS tests the eni information was correctly read from the acs -func TestENIFromACS(t *testing.T) { - acsENI := getTestACSENI() - eni, err := ENIFromACS(acsENI) - assert.NoError(t, err) - assert.NotNil(t, eni) - assert.Equal(t, aws.StringValue(acsENI.Ec2Id), eni.ID) - assert.Len(t, eni.IPV4Addresses, 1) - assert.Len(t, eni.GetIPV4Addresses(), 1) - assert.Equal(t, aws.StringValue(acsENI.Ipv4Addresses[0].PrivateAddress), eni.IPV4Addresses[0].Address) - assert.Equal(t, aws.BoolValue(acsENI.Ipv4Addresses[0].Primary), eni.IPV4Addresses[0].Primary) - assert.Equal(t, aws.StringValue(acsENI.MacAddress), eni.MacAddress) - assert.Len(t, eni.IPV6Addresses, 1) - assert.Len(t, eni.GetIPV6Addresses(), 1) - assert.Equal(t, aws.StringValue(acsENI.Ipv6Addresses[0].Address), eni.IPV6Addresses[0].Address) - assert.Len(t, eni.DomainNameServers, 2) - assert.Equal(t, defaultDNS, eni.DomainNameServers[0]) - assert.Equal(t, customDNS, eni.DomainNameServers[1]) - assert.Len(t, eni.DomainNameSearchList, 1) - assert.Equal(t, customSearchDomain, eni.DomainNameSearchList[0]) - assert.Equal(t, aws.StringValue(acsENI.PrivateDnsName), eni.PrivateDNSName) -} - -// TestValidateENIFromACS tests the validation of enis from acs -func TestValidateENIFromACS(t *testing.T) { - acsENI := getTestACSENI() - err := ValidateENI(acsENI) - assert.NoError(t, err) - - acsENI.Ipv6Addresses = nil - err = ValidateENI(acsENI) - assert.NoError(t, err) - - acsENI.Ipv4Addresses = nil - err = ValidateENI(acsENI) - assert.Error(t, err) -} - -func TestInvalidENIInterfaceVlanPropertyMissing(t *testing.T) { - acsENI := &ecsacs.ElasticNetworkInterface{ - InterfaceAssociationProtocol: aws.String(VLANInterfaceAssociationProtocol), - AttachmentArn: aws.String("arn"), - Ec2Id: aws.String("ec2id"), - Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ - { - Primary: aws.Bool(true), - PrivateAddress: aws.String("ipv4"), - }, - }, - SubnetGatewayIpv4Address: aws.String(ipv4GwWithPrefixLength), - Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ - { - Address: aws.String("ipv6"), - }, - }, - MacAddress: aws.String("mac"), - } - - err := ValidateENI(acsENI) - assert.Error(t, err) - -} - -func TestInvalidENIInvalidInterfaceAssociationProtocol(t *testing.T) { - acsENI := &ecsacs.ElasticNetworkInterface{ - InterfaceAssociationProtocol: aws.String("no-eni"), - AttachmentArn: aws.String("arn"), - Ec2Id: aws.String("ec2id"), - Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ - { - Primary: aws.Bool(true), - PrivateAddress: aws.String("ipv4"), - }, - }, - SubnetGatewayIpv4Address: aws.String(ipv4GwWithPrefixLength), - Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ - { - Address: aws.String("ipv6"), - }, - }, - MacAddress: aws.String("mac"), - } - err := ValidateENI(acsENI) - assert.Error(t, err) -} - -func TestInvalidSubnetGatewayAddress(t *testing.T) { - acsENI := getTestACSENI() - acsENI.SubnetGatewayIpv4Address = aws.String(ipv4Addr) - _, err := ENIFromACS(acsENI) - assert.Error(t, err) -} - -func getTestACSENI() *ecsacs.ElasticNetworkInterface { - return &ecsacs.ElasticNetworkInterface{ - AttachmentArn: aws.String("arn"), - Ec2Id: aws.String("ec2id"), - Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ - { - Primary: aws.Bool(true), - PrivateAddress: aws.String(ipv4Addr), - }, - }, - SubnetGatewayIpv4Address: aws.String(ipv4GwWithPrefixLength), - Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ - { - Address: aws.String(ipv6Addr)}, - }, - MacAddress: aws.String("mac"), - DomainNameServers: []*string{aws.String(defaultDNS), aws.String(customDNS)}, - DomainName: []*string{aws.String(customSearchDomain)}, - PrivateDnsName: aws.String("ip.region.compute.internal"), - } -} - -// TestV2NTunnelFromACS tests the ENI model created from ACS V2N interface payload. -func TestV2NTunnelFromACS(t *testing.T) { - v2nTunnelACS := &ecsacs.ElasticNetworkInterface{ - DomainNameServers: []*string{ - aws.String(v2nDnsIP), - }, - DomainName: []*string{ - aws.String(v2nDnsSearch), - }, - InterfaceTunnelProperties: &ecsacs.NetworkInterfaceTunnelProperties{ - TunnelId: aws.String(v2nVNI), - InterfaceIpAddress: aws.String(v2nDestinationIP), - }, - } - - // Test success case. - v2nEni, err := v2nTunnelFromACS(v2nTunnelACS) - require.NoError(t, err) - - assert.Equal(t, V2NInterfaceAssociationProtocol, v2nEni.InterfaceAssociationProtocol) - assert.Equal(t, DefaultGeneveInterfaceGateway, v2nEni.SubnetGatewayIPV4Address) - assert.Equal(t, DefaultGeneveInterfaceIPAddress, v2nEni.IPV4Addresses[0].Address) - assert.Equal(t, v2nDnsIP, v2nEni.DomainNameServers[0]) - assert.Equal(t, v2nDnsSearch, v2nEni.DomainNameSearchList[0]) - - assert.Equal(t, v2nVNI, v2nEni.TunnelProperties.ID) - assert.Equal(t, v2nDestinationIP, v2nEni.TunnelProperties.DestinationIPAddress) - - // Test failure cases. - v2nTunnelACS.InterfaceTunnelProperties.TunnelId = nil - _, err = v2nTunnelFromACS(v2nTunnelACS) - require.Error(t, err) - require.Equal(t, "tunnel ID not found in payload", err.Error()) - - v2nTunnelACS.InterfaceTunnelProperties.TunnelId = aws.String(v2nVNI) - v2nTunnelACS.InterfaceTunnelProperties.InterfaceIpAddress = nil - _, err = v2nTunnelFromACS(v2nTunnelACS) - require.Error(t, err) - require.Equal(t, "tunnel interface IP not found in payload", err.Error()) - - v2nTunnelACS.InterfaceTunnelProperties = nil - _, err = v2nTunnelFromACS(v2nTunnelACS) - require.Error(t, err) - assert.Equal(t, "interface tunnel properties not found in payload", err.Error()) -} - -// TestVETHPairFromACS tests the ENI model created from ACS VETH interface payload. -// It tests if the ENI model inherits the DNS config data from the peer interface -// and also verifies that an error is returned if the peer interface is also veth. -func TestVETHPairFromACS(t *testing.T) { - peerInterface := &ecsacs.ElasticNetworkInterface{ - Name: aws.String(vethPeerInterfaceName), - DomainNameServers: []*string{aws.String("10.0.23.2")}, - DomainName: []*string{aws.String("amazon.com")}, - } - - vethACS := &ecsacs.ElasticNetworkInterface{ - InterfaceVethProperties: &ecsacs.NetworkInterfaceVethProperties{ - PeerInterface: aws.String(vethPeerInterfaceName), - }, - } - - vethInterface, err := vethPairFromACS(vethACS, peerInterface) - require.NoError(t, err) - - assert.Equal(t, VETHInterfaceAssociationProtocol, vethInterface.InterfaceAssociationProtocol) - assert.Equal(t, vethPeerInterfaceName, vethInterface.VETHProperties.PeerInterfaceName) - assert.Equal(t, vethInterface.DomainNameServers[0], "10.0.23.2") - assert.Equal(t, vethInterface.DomainNameSearchList[0], "amazon.com") - - peerInterface.InterfaceAssociationProtocol = aws.String(VETHInterfaceAssociationProtocol) - _, err = vethPairFromACS(vethACS, peerInterface) - require.Error(t, err) - assert.Equal(t, "peer interface cannot be veth", err.Error()) -} diff --git a/ecs-agent/netlib/model/tasknetworkconfig/common_test.go b/ecs-agent/netlib/model/tasknetworkconfig/common_test.go index 35da4a995f..cb0a41cb0b 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/common_test.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/common_test.go @@ -4,6 +4,7 @@ import ni "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterfa const ( primaryNetNSName = "primary-netns" + primaryNetNSPath = "primary-path" secondaryNetNSName = "secondary-netns" primaryInterfaceName = "primary-interface" secondaryInterfaceName = "secondary-interface" @@ -33,12 +34,14 @@ func getTestNetworkNamespaces() []*NetworkNamespace { func getTestNetworkInterfaces() []*ni.NetworkInterface { return []*ni.NetworkInterface{ { - Name: secondaryInterfaceName, - Index: 1, + Name: secondaryInterfaceName, + Default: false, + Index: 1, }, { - Name: primaryInterfaceName, - Index: 0, + Name: primaryInterfaceName, + Default: true, + Index: 0, }, } } diff --git a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go index ae6d9103e4..2a0ccd085e 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace.go @@ -1,8 +1,10 @@ package tasknetworkconfig import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/appmesh" "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status" ) // NetworkNamespace is model representing each network namespace. @@ -19,15 +21,41 @@ type NetworkNamespace struct { // TODO: Add Service Connect model here once it is moved under the netlib package. - KnownState string - DesiredState string + KnownState status.NetworkStatus + DesiredState status.NetworkStatus +} + +func NewNetworkNamespace( + netNSName string, + netNSPath string, + index int, + proxyConfig *ecsacs.ProxyConfiguration, + networkInterfaces ...*networkinterface.NetworkInterface) (*NetworkNamespace, error) { + netNS := &NetworkNamespace{ + Name: netNSName, + Path: netNSPath, + Index: index, + NetworkInterfaces: networkInterfaces, + KnownState: status.NetworkNone, + DesiredState: status.NetworkReadyPull, + } + + var err error + if proxyConfig != nil { + netNS.AppMeshConfig, err = appmesh.AppMeshFromACS(proxyConfig) + if err != nil { + return nil, err + } + } + + return netNS, nil } // GetPrimaryInterface returns the network interface that has the index value of 0 within // the network namespace. func (ns NetworkNamespace) GetPrimaryInterface() *networkinterface.NetworkInterface { for _, ni := range ns.NetworkInterfaces { - if ni.Index == 0 { + if ni.Default { return ni } } diff --git a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go index 6af1b83865..092d8083a6 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/network_namespace_test.go @@ -4,8 +4,6 @@ package tasknetworkconfig import ( - "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" - "testing" "github.com/stretchr/testify/assert" @@ -13,19 +11,29 @@ import ( func TestNetworkNamespace_GetPrimaryInterface(t *testing.T) { netns := &NetworkNamespace{ - NetworkInterfaces: []*networkinterface.NetworkInterface{ - { - Index: 1, - Name: secondaryInterfaceName, - }, - { - Index: 0, - Name: primaryInterfaceName, - }, - }, + NetworkInterfaces: getTestNetworkInterfaces(), } assert.Equal(t, primaryInterfaceName, netns.GetPrimaryInterface().Name) netns = &NetworkNamespace{} assert.Empty(t, netns.GetPrimaryInterface()) } + +// TestNewNetworkNamespace tests creation of a new NetworkNamespace object. +func TestNewNetworkNamespace(t *testing.T) { + netIFs := getTestNetworkInterfaces() + netns, err := NewNetworkNamespace( + primaryNetNSName, + primaryNetNSPath, + 0, + nil, + netIFs...) + assert.NoError(t, err) + assert.Equal(t, 2, len(netns.NetworkInterfaces)) + assert.Equal(t, primaryNetNSName, netns.Name) + assert.Equal(t, primaryNetNSPath, netns.Path) + assert.Equal(t, 0, netns.Index) + assert.Empty(t, netns.AppMeshConfig) + assert.Equal(t, *netIFs[0], *netns.NetworkInterfaces[0]) + assert.Equal(t, *netIFs[1], *netns.NetworkInterfaces[1]) +} diff --git a/ecs-agent/netlib/model/tasknetworkconfig/task_network_config.go b/ecs-agent/netlib/model/tasknetworkconfig/task_network_config.go index 61ce6c0ddf..1a0caafdab 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/task_network_config.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/task_network_config.go @@ -1,6 +1,11 @@ package tasknetworkconfig -import ni "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" +import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/ecs_client/model/ecs" + ni "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" + + "github.com/pkg/errors" +) // TaskNetworkConfig is the top level network data structure associated with a task. type TaskNetworkConfig struct { @@ -8,6 +13,20 @@ type TaskNetworkConfig struct { NetworkMode string } +func New(networkMode string, netNSs ...*NetworkNamespace) (*TaskNetworkConfig, error) { + if networkMode != ecs.NetworkModeAwsvpc && + networkMode != ecs.NetworkModeBridge && + networkMode != ecs.NetworkModeHost && + networkMode != ecs.NetworkModeNone { + return nil, errors.New("invalid network mode: " + networkMode) + } + + return &TaskNetworkConfig{ + NetworkNamespaces: netNSs, + NetworkMode: networkMode, + }, nil +} + // GetPrimaryInterface returns the interface with index 0 inside the network namespace // with index 0 associated with the task's network config. func (tnc *TaskNetworkConfig) GetPrimaryInterface() *ni.NetworkInterface { diff --git a/ecs-agent/netlib/model/tasknetworkconfig/task_network_config_test.go b/ecs-agent/netlib/model/tasknetworkconfig/task_network_config_test.go index 1e2716df33..e3c81d26fe 100644 --- a/ecs-agent/netlib/model/tasknetworkconfig/task_network_config_test.go +++ b/ecs-agent/netlib/model/tasknetworkconfig/task_network_config_test.go @@ -4,6 +4,7 @@ package tasknetworkconfig import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/ecs_client/model/ecs" "github.com/stretchr/testify/assert" "testing" @@ -26,3 +27,42 @@ func TestTaskNetworkConfig_GetPrimaryNetNS(t *testing.T) { testNetConfig = &TaskNetworkConfig{} assert.Nil(t, testNetConfig.GetPrimaryNetNS()) } + +// TestNewTaskNetConfig tests creation of TaskNetworkConfig out of +// a given set of NetworkNamespace objects. +func TestNewTaskNetConfig(t *testing.T) { + protos := []string{ + ecs.NetworkModeAwsvpc, + ecs.NetworkModeHost, + ecs.NetworkModeBridge, + ecs.NetworkModeNone, + } + for _, proto := range protos { + _, err := New(proto, nil) + assert.NoError(t, err) + } + + _, err := New("invalid-protocol", nil) + assert.Error(t, err) + + primaryNetNS := "primary-netns" + secondaryNetNS := "secondary-netns" + netNSs := []*NetworkNamespace{ + { + Name: primaryNetNS, + Index: 0, + }, + { + Name: secondaryNetNS, + Index: 1, + }, + } + + taskNetConfig, err := New( + ecs.NetworkModeAwsvpc, + netNSs...) + assert.NoError(t, err) + assert.Equal(t, 2, len(taskNetConfig.NetworkNamespaces)) + assert.Equal(t, *netNSs[0], *taskNetConfig.NetworkNamespaces[0]) + assert.Equal(t, *netNSs[1], *taskNetConfig.NetworkNamespaces[1]) +} diff --git a/ecs-agent/netlib/network_builder.go b/ecs-agent/netlib/network_builder.go new file mode 100644 index 0000000000..053705ff09 --- /dev/null +++ b/ecs-agent/netlib/network_builder.go @@ -0,0 +1,46 @@ +package netlib + +import ( + "context" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/ecscni" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/platform" + + "github.com/pkg/errors" +) + +type NetworkBuilder interface { + BuildTaskNetworkConfiguration(taskID string, taskPayload *ecsacs.Task) (*tasknetworkconfig.TaskNetworkConfig, error) + + Start(ctx context.Context, taskNetConfig *tasknetworkconfig.TaskNetworkConfig) error + + Stop(ctx context.Context, taskNetConfig *tasknetworkconfig.TaskNetworkConfig) error +} + +type networkBuilder struct { + platformAPI platform.API +} + +func NewNetworkBuilder(platformString string) (NetworkBuilder, error) { + pAPI, err := platform.NewPlatform(platformString, ecscni.NewNetNSUtil()) + if err != nil { + return nil, errors.Wrap(err, "failed to instantiate network builder") + } + return &networkBuilder{ + platformAPI: pAPI, + }, nil +} + +func (nb *networkBuilder) BuildTaskNetworkConfiguration(taskID string, taskPayload *ecsacs.Task) (*tasknetworkconfig.TaskNetworkConfig, error) { + return nb.platformAPI.BuildTaskNetworkConfiguration(taskID, taskPayload) +} + +func (nb *networkBuilder) Start(ctx context.Context, netConfig *tasknetworkconfig.TaskNetworkConfig) error { + return nil +} + +func (nb *networkBuilder) Stop(ctx context.Context, netConfig *tasknetworkconfig.TaskNetworkConfig) error { + return nil +} diff --git a/ecs-agent/netlib/network_builder_linux_test.go b/ecs-agent/netlib/network_builder_linux_test.go new file mode 100644 index 0000000000..07ddcd9081 --- /dev/null +++ b/ecs-agent/netlib/network_builder_linux_test.go @@ -0,0 +1,279 @@ +//go:build !windows && unit +// +build !windows,unit + +package netlib + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/ecs_client/model/ecs" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/status" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/platform" + + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/require" +) + +func TestNewNetworkBuilder(t *testing.T) { + nbi, err := NewNetworkBuilder(platform.WarmpoolPlatform) + nb := nbi.(*networkBuilder) + require.NoError(t, err) + require.NotNil(t, nb.platformAPI) + + nbi, err = NewNetworkBuilder("invalid-platform") + require.Error(t, err) + require.Nil(t, nbi) +} + +// TestNetworkBuilder_BuildTaskNetworkConfiguration verifies for all known use cases, +// the network builder is able to translate the input task payload into the desired +// network data models. +func TestNetworkBuilder_BuildTaskNetworkConfiguration(t *testing.T) { + t.Run("containerd-default", getTestFunc(getSingleNetNSAWSVPCTestData)) + t.Run("containerd-multi-interface", getTestFunc(getSingleNetNSMultiIfaceAWSVPCTestData)) + t.Run("containerd-multi-netns", getTestFunc(getMultiNetNSMultiIfaceAWSVPCTestData)) +} + +// getTestFunc returns a test function that verifies the capability of the networkBuilder +// to translate a given input task payload into desired network data models. +func getTestFunc(dataGenF func(string) (input *ecsacs.Task, expected tasknetworkconfig.TaskNetworkConfig)) func(*testing.T) { + + return func(t *testing.T) { + // Create a networkBuilder for the warmpool platform. + netBuilder, err := NewNetworkBuilder(platform.WarmpoolPlatform) + require.NoError(t, err) + + // Generate input task payload and a reference to verify the output with. + taskPayload, expectedConfig := dataGenF(taskID) + + // Invoke networkBuilder function for building the task network config. + actualConfig, err := netBuilder.BuildTaskNetworkConfiguration(taskID, taskPayload) + require.NoError(t, err) + + // Convert the obtained output and the reference data into json data to make it + // easier to compare. + expected, err := json.Marshal(expectedConfig) + require.NoError(t, err) + actual, err := json.Marshal(actualConfig) + require.NoError(t, err) + + require.Equal(t, string(expected), string(actual)) + } +} + +// getSingleNetNSAWSVPCTestData returns a task payload and a task network config +// to be used the input and reference result for tests. The reference object will +// has only one network namespace and network interface. +func getSingleNetNSAWSVPCTestData(testTaskID string) (*ecsacs.Task, tasknetworkconfig.TaskNetworkConfig) { + enis, netIfs := getTestInterfacesData() + taskPayload := &ecsacs.Task{ + NetworkMode: aws.String(ecs.NetworkModeAwsvpc), + ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{enis[0]}, + } + + netNSName := fmt.Sprintf(netNSNamePattern, testTaskID, eniName) + netNSPath := netNSPathDir + netNSName + taskNetConfig := tasknetworkconfig.TaskNetworkConfig{ + NetworkMode: ecs.NetworkModeAwsvpc, + NetworkNamespaces: []*tasknetworkconfig.NetworkNamespace{ + { + Name: netNSName, + Path: netNSPath, + Index: 0, + NetworkInterfaces: []*networkinterface.NetworkInterface{ + &netIfs[0], + }, + KnownState: status.NetworkNone, + DesiredState: status.NetworkReadyPull, + }, + }, + } + + return taskPayload, taskNetConfig +} + +// getSingleNetNSMultiIfaceAWSVPCTestData returns test data for EKS like use cases. +func getSingleNetNSMultiIfaceAWSVPCTestData(testTaskID string) (*ecsacs.Task, tasknetworkconfig.TaskNetworkConfig) { + taskPayload, taskNetConfig := getSingleNetNSAWSVPCTestData(testTaskID) + enis, netIfs := getTestInterfacesData() + secondIFPayload := enis[1] + secondIF := &netIfs[1] + taskPayload.ElasticNetworkInterfaces = append(taskPayload.ElasticNetworkInterfaces, secondIFPayload) + netNS := taskNetConfig.NetworkNamespaces[0] + netNS.NetworkInterfaces = append(netNS.NetworkInterfaces, secondIF) + + return taskPayload, taskNetConfig +} + +// getMultiNetNSMultiIfaceAWSVPCTestData returns test data for multiple netns and net interface cases. +func getMultiNetNSMultiIfaceAWSVPCTestData(testTaskID string) (*ecsacs.Task, tasknetworkconfig.TaskNetworkConfig) { + ifName1 := "primary-eni" + ifName2 := "secondary-eni" + enis, netIfs := getTestInterfacesData() + enis[0].Name = aws.String(ifName1) + enis[1].Name = aws.String(ifName2) + + netIfs[0].Name = ifName1 + netIfs[1].Name = ifName2 + netIfs[1].Default = true + + taskPayload := &ecsacs.Task{ + NetworkMode: aws.String(ecs.NetworkModeAwsvpc), + ElasticNetworkInterfaces: enis, + Containers: []*ecsacs.Container{ + { + NetworkInterfaceNames: []*string{aws.String(ifName1)}, + }, + { + NetworkInterfaceNames: []*string{aws.String(ifName2)}, + }, + { + NetworkInterfaceNames: []*string{aws.String(ifName1)}, + }, + { + NetworkInterfaceNames: []*string{aws.String(ifName2)}, + }, + }, + } + + primaryNetNSName := fmt.Sprintf(netNSNamePattern, testTaskID, ifName1) + primaryNetNSPath := netNSPathDir + primaryNetNSName + secondaryNetNSName := fmt.Sprintf(netNSNamePattern, testTaskID, ifName2) + secondaryNetNSPath := netNSPathDir + secondaryNetNSName + + taskNetConfig := tasknetworkconfig.TaskNetworkConfig{ + NetworkMode: ecs.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, + }, + }, + } + + return taskPayload, taskNetConfig +} + +func getTestInterfacesData() ([]*ecsacs.ElasticNetworkInterface, []networkinterface.NetworkInterface) { + // interfacePayloads have multiple interfaces as they are sent by ACS + // that can be used as input data for tests. + interfacePayloads := []*ecsacs.ElasticNetworkInterface{ + { + Ec2Id: aws.String(eniID), + MacAddress: aws.String(eniMAC), + PrivateDnsName: aws.String(dnsName), + DomainNameServers: []*string{aws.String(nameServer)}, + Index: aws.Int64(0), + Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(ipv4Addr), + }, + }, + Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ + { + Address: aws.String(ipv6Addr), + }, + }, + SubnetGatewayIpv4Address: aws.String(subnetGatewayCIDR), + InterfaceAssociationProtocol: aws.String(networkinterface.DefaultInterfaceAssociationProtocol), + DomainName: []*string{aws.String(searchDomainName)}, + }, + { + Ec2Id: aws.String(eniID2), + MacAddress: aws.String(eniMAC2), + PrivateDnsName: aws.String(dnsName), + DomainNameServers: []*string{aws.String(nameServer2)}, + Index: aws.Int64(1), + Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(ipv4Addr2), + }, + }, + Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ + { + Address: aws.String(ipv6Addr2), + }, + }, + SubnetGatewayIpv4Address: aws.String(subnetGatewayCIDR2), + InterfaceAssociationProtocol: aws.String(networkinterface.DefaultInterfaceAssociationProtocol), + DomainName: []*string{aws.String(searchDomainName)}, + }, + } + + networkInterfaces := []networkinterface.NetworkInterface{ + { + ID: eniID, + MacAddress: eniMAC, + Name: eniName, + IPV4Addresses: []*networkinterface.IPV4Address{ + { + Primary: true, + Address: ipv4Addr, + }, + }, + IPV6Addresses: []*networkinterface.IPV6Address{ + { + Address: ipv6Addr, + }, + }, + SubnetGatewayIPV4Address: subnetGatewayCIDR, + DomainNameServers: []string{nameServer}, + DomainNameSearchList: []string{searchDomainName}, + PrivateDNSName: dnsName, + InterfaceAssociationProtocol: networkinterface.DefaultInterfaceAssociationProtocol, + Index: int64(0), + Default: true, + KnownStatus: status.NetworkNone, + DesiredStatus: status.NetworkReadyPull, + }, + { + ID: eniID2, + MacAddress: eniMAC2, + Name: eniName2, + IPV4Addresses: []*networkinterface.IPV4Address{ + { + Primary: true, + Address: ipv4Addr2, + }, + }, + IPV6Addresses: []*networkinterface.IPV6Address{ + { + Address: ipv6Addr2, + }, + }, + SubnetGatewayIPV4Address: subnetGatewayCIDR2, + DomainNameServers: []string{nameServer2}, + DomainNameSearchList: []string{searchDomainName}, + PrivateDNSName: dnsName, + InterfaceAssociationProtocol: networkinterface.DefaultInterfaceAssociationProtocol, + Index: int64(1), + KnownStatus: status.NetworkNone, + DesiredStatus: status.NetworkReadyPull, + }, + } + + return interfacePayloads, networkInterfaces +} diff --git a/ecs-agent/netlib/platform/api.go b/ecs-agent/netlib/platform/api.go new file mode 100644 index 0000000000..0e6f4f7b15 --- /dev/null +++ b/ecs-agent/netlib/platform/api.go @@ -0,0 +1,33 @@ +package platform + +import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" +) + +// API declares a set of methods that requires platform specific implementations. +type API interface { + // BuildTaskNetworkConfiguration translates network data in task payload sent by ACS + // into the task network configuration data structure internal to the agent. + BuildTaskNetworkConfiguration( + taskID string, + taskPayload *ecsacs.Task) (*tasknetworkconfig.TaskNetworkConfig, error) + + // CreateNetNS creates a network namespace with the specified name. + CreateNetNS(netNSName string) error + + // DeleteNetNS deletes the specified network namespace. + DeleteNetNS(netnsName string) error + + // CreateDNSConfig creates the following DNS config files depending on the + // task network configuration: + // 1. resolv.conf + // 2. hosts + // 3. hostname + // These files are then copied into desired locations so that containers will + // have access to the accurate DNS configuration information. + CreateDNSConfig(taskNetConfig *tasknetworkconfig.TaskNetworkConfig) error + + // GetNetNSPath returns the path of a network namespace. + GetNetNSPath(netNSName string) string +} diff --git a/ecs-agent/netlib/platform/common_linux.go b/ecs-agent/netlib/platform/common_linux.go new file mode 100644 index 0000000000..ae44b780f6 --- /dev/null +++ b/ecs-agent/netlib/platform/common_linux.go @@ -0,0 +1,207 @@ +//go:build !windows +// +build !windows + +package platform + +import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/ecs_client/model/ecs" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/ecscni" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" + "github.com/aws/aws-sdk-go/aws" + "github.com/pkg/errors" +) + +const ( + // Identifiers for each platform we support. + WarmpoolDebugPlatform = "ec2-debug-warmpool" + FirecrackerDebugPlatform = "ec2-debug-firecracker" + WarmpoolPlatform = "warmpool" + FirecrackerPlatform = "firecracker" + + // indexHighValue is a placeholder value used while finding + // interface with lowest index in from the ACS payload. + // It is assigned 100 because it is an unrealistically high + // value for interface index. + indexHighValue = 100 +) + +// common will be embedded within every implementation of the platform API. +// It contains all fields and methods that can be commonly used by all +// platforms. +type common struct { + nsUtil ecscni.NetNSUtil +} + +// NewPlatform creates an implementation of the platform API depending on the +// platform type where the agent is executing. +func NewPlatform( + platformString string, + nsUtil ecscni.NetNSUtil) (API, error) { + commonPlatform := common{ + nsUtil: nsUtil, + } + + // TODO: implement remaining platforms - FoF, ECS on EC2. + switch platformString { + case WarmpoolPlatform: + return &containerd{ + common: commonPlatform, + }, nil + } + return nil, errors.New("invalid platform: " + platformString) +} + +// BuildTaskNetworkConfiguration translates network data in task payload sent by ACS +// into the task network configuration data structure internal to the agent. +func (c *common) BuildTaskNetworkConfiguration( + taskID string, + taskPayload *ecsacs.Task) (*tasknetworkconfig.TaskNetworkConfig, error) { + mode := aws.StringValue(taskPayload.NetworkMode) + var netNSs []*tasknetworkconfig.NetworkNamespace + var err error + switch mode { + case ecs.NetworkModeAwsvpc: + netNSs, err = c.buildAWSVPCNetworkNamespaces(taskID, taskPayload) + if err != nil { + return nil, errors.Wrap(err, "failed to translate network configuration") + } + case ecs.NetworkModeBridge: + return nil, errors.New("not implemented") + case ecs.NetworkModeHost: + return nil, errors.New("not implemented") + case ecs.NetworkModeNone: + return nil, errors.New("not implemented") + default: + return nil, errors.New("invalid network mode: " + mode) + } + + return &tasknetworkconfig.TaskNetworkConfig{ + NetworkNamespaces: netNSs, + NetworkMode: mode, + }, nil +} + +func (c *common) GetNetNSPath(netNSName string) string { + return c.nsUtil.GetNetNSPath(netNSName) +} + +// buildAWSVPCNetworkNamespaces returns list of NetworkNamespace which will be used to +// create the task's network configuration. All cases except those for FoF is covered by +// this method. FoF requires a separate specific implementation because the network setup +// is different due to the presence of the microVM. +// Use cases covered by this method are: +// 1. Single interface, network namespace (the only externally available config). +// 2. Single netns, multiple interfaces (For a non-managed multi-ENI experience. Eg EKS use case). +// 3. Multiple netns, multiple interfaces (future use case for internal customer who need +// a managed multi-ENI experience). +func (c *common) buildAWSVPCNetworkNamespaces(taskID string, + taskPayload *ecsacs.Task) ([]*tasknetworkconfig.NetworkNamespace, error) { + if len(taskPayload.ElasticNetworkInterfaces) == 0 { + return nil, errors.New("interfaces list cannot be empty") + } + // If task payload has only one interface, the network configuration is + // straight forward. It will have only one network namespace containing + // the corresponding network interface. + // Empty Name fields in network interface names indicate that all + // interfaces share the same network namespace. This use case is + // utilized by certain internal teams like EKS on Fargate. + if len(taskPayload.ElasticNetworkInterfaces) == 1 || + aws.StringValue(taskPayload.ElasticNetworkInterfaces[0].Name) == "" { + primaryNetNS, err := c.buildSingleNSNetConfig(taskID, + 0, + taskPayload.ElasticNetworkInterfaces, + taskPayload.ProxyConfiguration) + if err != nil { + return nil, err + } + + return []*tasknetworkconfig.NetworkNamespace{primaryNetNS}, nil + } + + // Create a map for easier lookup of ENIs by their names. + ifNameMap := make(map[string]*ecsacs.ElasticNetworkInterface, len(taskPayload.ElasticNetworkInterfaces)) + for _, iface := range taskPayload.ElasticNetworkInterfaces { + ifNameMap[networkinterface.GetInterfaceName(iface)] = iface + } + + // Proxy configuration is not supported yet in a multi-ENI / multi-NetNS task. + if taskPayload.ProxyConfiguration != nil { + return nil, errors.New("unexpected proxy config found") + } + + // The number of network namespaces required to create depends on the + // number of unique interface names list across all container definitions + // in the task payload. Meaning if two containers are linked with the same + // set of network interface names, both those containers share the same namespace. + // If not, they reside in two different namespaces. Also, an interface can only + // belong to one NetworkNamespace object. + + var netNSs []*tasknetworkconfig.NetworkNamespace + nsIndex := 0 + // Loop through each container definition and their network interfaces. + for _, container := range taskPayload.Containers { + // ifaces holds all interfaces associated with a particular container. + var ifaces []*ecsacs.ElasticNetworkInterface + for _, ifNameP := range container.NetworkInterfaceNames { + ifName := aws.StringValue(ifNameP) + if iface := ifNameMap[ifName]; iface != nil { + ifaces = append(ifaces, iface) + // Remove ENI from map to indicate that the ENI is assigned to + // a namespace. + delete(ifNameMap, ifName) + } else { + // If the ENI does not exist in the lookup map, it means the ENI + // is already assigned to a namespace. The container will be run + // in the same namespace. + break + } + } + + if len(ifaces) == 0 { + continue + } + + netNS, err := c.buildSingleNSNetConfig(taskID, nsIndex, ifaces, nil) + if err != nil { + return nil, err + } + netNSs = append(netNSs, netNS) + nsIndex += 1 + } + + return netNSs, nil +} + +func (c *common) buildSingleNSNetConfig( + taskID string, + index int, + networkInterfaces []*ecsacs.ElasticNetworkInterface, + proxyConfig *ecsacs.ProxyConfiguration) (*tasknetworkconfig.NetworkNamespace, error) { + var primaryIF *networkinterface.NetworkInterface + var ifaces []*networkinterface.NetworkInterface + lowestIdx := int64(indexHighValue) + for _, ni := range networkInterfaces { + iface, err := networkinterface.New(ni, "", nil) + if err != nil { + return nil, err + } + if aws.Int64Value(ni.Index) < lowestIdx { + primaryIF = iface + lowestIdx = aws.Int64Value(ni.Index) + } + ifaces = append(ifaces, iface) + } + + primaryIF.Default = true + netNSName := networkinterface.NetNSName(taskID, primaryIF.Name) + netNSPath := c.GetNetNSPath(netNSName) + + return tasknetworkconfig.NewNetworkNamespace( + netNSName, + netNSPath, + index, + proxyConfig, + ifaces...) +} diff --git a/ecs-agent/netlib/platform/common_linux_test.go b/ecs-agent/netlib/platform/common_linux_test.go new file mode 100644 index 0000000000..eeae1ffd37 --- /dev/null +++ b/ecs-agent/netlib/platform/common_linux_test.go @@ -0,0 +1,18 @@ +//go:build !windows && unit +// +build !windows,unit + +package platform + +import ( + "github.com/stretchr/testify/assert" + + "testing" +) + +func TestNewPlatform(t *testing.T) { + _, err := NewPlatform(WarmpoolPlatform, nil) + assert.NoError(t, err) + + _, err = NewPlatform("invalid-platform", nil) + assert.Error(t, err) +} diff --git a/ecs-agent/netlib/platform/containerd_linux.go b/ecs-agent/netlib/platform/containerd_linux.go new file mode 100644 index 0000000000..7e08e62c7f --- /dev/null +++ b/ecs-agent/netlib/platform/containerd_linux.go @@ -0,0 +1,31 @@ +package platform + +import ( + "context" + + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" +) + +// containerd implements platform API methods for non-firecrakcer infrastructure. +type containerd struct { + common +} + +func (c *containerd) CreateNetNS(netNSName string) error { + return nil +} + +func (c *containerd) ConfigureNamespaces( + ctx context.Context, + netNamespaces []*tasknetworkconfig.NetworkNamespace, + networkMode string) error { + return nil +} + +func (c *containerd) DeleteNetNS(netnsName string) error { + return nil +} + +func (c *containerd) CreateDNSConfig(taskNetConfig *tasknetworkconfig.TaskNetworkConfig) error { + return nil +} diff --git a/ecs-agent/netlib/platform/containerd_windows.go b/ecs-agent/netlib/platform/containerd_windows.go new file mode 100644 index 0000000000..5c4403580b --- /dev/null +++ b/ecs-agent/netlib/platform/containerd_windows.go @@ -0,0 +1,42 @@ +//go:build windows +// +build windows + +package platform + +import ( + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/ecscni" + "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/tasknetworkconfig" +) + +type containerd struct { + nsUtil ecscni.NetNSUtil +} + +func NewPlatform( + platformString string, + nsUtil ecscni.NetNSUtil) (API, error) { + return nil, nil +} + +func (c *containerd) BuildTaskNetworkConfiguration( + taskID string, + taskPayload *ecsacs.Task) (*tasknetworkconfig.TaskNetworkConfig, error) { + return nil, nil +} + +func (c *containerd) CreateNetNS(netNSName string) error { + return nil +} + +func (c *containerd) DeleteNetNS(netnsName string) error { + return nil +} + +func (c *containerd) CreateDNSConfig(taskNetConfig *tasknetworkconfig.TaskNetworkConfig) error { + return nil +} + +func (c *containerd) GetNetNSPath(netNSName string) string { + return "" +}