diff --git a/agent/acs/session/payload_responder.go b/agent/acs/session/payload_responder.go index 770de32667c..4656c5f3af4 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 72667e7d03b..3ed4a1da313 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 6b10dde38e5..00000000000 --- 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 00000000000..b6fdb9d2d84 --- /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 bfc197d7bd7..23f70f1d140 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 00000000000..df29b873186 --- /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 72667e7d03b..3ed4a1da313 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 6b10dde38e5..00000000000 --- 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 241d586119e..00000000000 --- 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 35da4a995fc..cb0a41cb0bd 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 ae6d9103e43..2a0ccd085ed 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 6af1b838658..092d8083a6d 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 61ce6c0ddfb..1a0caafdab0 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 1e2716df339..e3c81d26fe1 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 00000000000..053705ff09c --- /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 00000000000..07ddcd9081c --- /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 00000000000..0e6f4f7b152 --- /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 00000000000..ae44b780f66 --- /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 00000000000..eeae1ffd379 --- /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 00000000000..7e08e62c7f1 --- /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 00000000000..5c4403580b5 --- /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 "" +}