Skip to content

Commit

Permalink
Merge pull request #2915 from rawahars/feature/awsvpc-windows
Browse files Browse the repository at this point in the history
Enable ECS EC2 task networking for Windows tasks
  • Loading branch information
angelcar authored Jun 29, 2021
2 parents 0957502 + 3495999 commit d952d4c
Show file tree
Hide file tree
Showing 91 changed files with 6,587 additions and 2,170 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
$Env:GOPATH = "$Env:GITHUB_WORKSPACE"
cd "$Env:GITHUB_WORKSPACE"
cd "src/github.com/aws/amazon-ecs-agent"
go test -v -race -tags unit -timeout 40s ./agent/...
go test -race -tags unit -timeout 40s ./agent/...
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# language governing permissions and limitations under the License.

USERID=$(shell id -u)
# default value of TARGET_OS
TARGET_OS=linux

.PHONY: all gobuild static xplatform-build docker release certs test clean netkitten test-registry benchmark-test gogenerate run-integ-tests pause-container get-cni-sources cni-plugins test-artifacts
BUILD_PLATFORM:=$(shell uname -m)
Expand Down Expand Up @@ -181,6 +183,12 @@ ECS_CNI_REPOSITORY_REVISION=master
ECS_CNI_REPOSITORY_SRC_DIR=$(PWD)/amazon-ecs-cni-plugins
VPC_CNI_REPOSITORY_SRC_DIR=$(PWD)/amazon-vpc-cni-plugins

# Variable to store the platform specific dockerfile for vpc-cni-plugins
VPC_CNI_REPOSITORY_DOCKER_FILE=scripts/dockerfiles/Dockerfile.buildVPCCNIPlugins
ifeq (${TARGET_OS}, windows)
VPC_CNI_REPOSITORY_DOCKER_FILE=scripts/dockerfiles/Dockerfile.buildWindowsVPCCNIPlugins
endif

get-cni-sources:
git submodule update --init --recursive

Expand All @@ -196,11 +204,11 @@ build-ecs-cni-plugins:
@echo "Built amazon-ecs-cni-plugins successfully."

build-vpc-cni-plugins:
@docker build --build-arg GOARCH=$(GOARCH) --build-arg GO_VERSION=$(GO_VERSION) -f scripts/dockerfiles/Dockerfile.buildVPCCNIPlugins -t "amazon/amazon-ecs-build-vpc-cni-plugins:make" .
@docker build --build-arg GOARCH=$(GOARCH) --build-arg GO_VERSION=$(GO_VERSION) -f $(VPC_CNI_REPOSITORY_DOCKER_FILE) -t "amazon/amazon-ecs-build-vpc-cni-plugins:make" .
docker run --rm --net=none \
-e GIT_SHORT_HASH=$(shell cd $(VPC_CNI_REPOSITORY_SRC_DIR) && git rev-parse --short=8 HEAD) \
-u "$(USERID)" \
-v "$(PWD)/out/amazon-vpc-cni-plugins:/go/src/github.com/aws/amazon-vpc-cni-plugins/build/linux_$(GOARCH)" \
-v "$(PWD)/out/amazon-vpc-cni-plugins:/go/src/github.com/aws/amazon-vpc-cni-plugins/build/${TARGET_OS}_$(GOARCH)" \
-v "$(VPC_CNI_REPOSITORY_SRC_DIR):/go/src/github.com/aws/amazon-vpc-cni-plugins" \
"amazon/amazon-ecs-build-vpc-cni-plugins:make"
@echo "Built amazon-vpc-cni-plugins successfully."
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ The following targets are available. Each may be run with `make <target>`.

### Standalone (on Windows)

The Amazon ECS Container Agent may be built by typing `go build -o amazon-ecs-agent.exe ./agent`.
The Amazon ECS Container Agent may be built by invoking `scripts\build_agent.ps1`

### Scripts (on Windows)

Expand Down
38 changes: 38 additions & 0 deletions agent/api/eni/eni.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
"fmt"
"net"
"strings"
"sync"

"github.com/cihub/seelog"

"github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs"
"github.com/aws/aws-sdk-go/aws"
Expand All @@ -27,6 +30,9 @@ import (
type ENI struct {
// ID is the id of eni
ID string `json:"ec2Id"`
// LinkName is the name of the ENI on the instance.
// Currently, this field is being used only for Windows and is used during task networking setup.
LinkName string
// MacAddress is the mac address of the eni
MacAddress string
// IPV4Addresses is the ipv4 address associated with the eni
Expand Down Expand Up @@ -55,6 +61,9 @@ type ENI struct {
ipv4SubnetPrefixLength string
ipv4SubnetCIDRBlock string
ipv6SubnetCIDRBlock string

// guard protects access to fields of this struct.
guard sync.RWMutex
}

// InterfaceVlanProperties contains information for an interface that
Expand All @@ -79,6 +88,11 @@ const (
IPv6SubnetPrefixLength = "64"
)

var (
// netInterfaces is the Interfaces() method of net package.
netInterfaces = net.Interfaces
)

// GetIPV4Addresses returns the list of IPv4 addresses assigned to the ENI.
func (eni *ENI) GetIPV4Addresses() []string {
var addresses []string
Expand Down Expand Up @@ -181,6 +195,30 @@ func (eni *ENI) GetHostname() string {
return eni.PrivateDNSName
}

// GetLinkName returns the name of the ENI on the instance.
func (eni *ENI) GetLinkName() string {
eni.guard.Lock()
defer eni.guard.Unlock()

if eni.LinkName == "" {
// Find all interfaces on the instance.
ifaces, err := netInterfaces()
if err != nil {
seelog.Errorf("Failed to find link name: %v.", err)
return ""
}
// Iterate over the list and find the interface with the ENI's MAC address.
for _, iface := range ifaces {
if strings.EqualFold(eni.MacAddress, iface.HardwareAddr.String()) {
eni.LinkName = iface.Name
break
}
}
}

return eni.LinkName
}

// IsStandardENI returns true if the ENI is a standard/regular ENI. That is, if it
// has its association protocol as standard. To be backwards compatible, if the
// association protocol is not set for an ENI, it's considered a standard ENI as well.
Expand Down
40 changes: 40 additions & 0 deletions agent/api/eni/eni_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package eni

import (
"net"
"testing"

"github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs"
"github.com/aws/aws-sdk-go/aws"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

Expand All @@ -28,6 +30,8 @@ const (
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"
Expand Down Expand Up @@ -58,6 +62,20 @@ var (
},
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) {
Expand Down Expand Up @@ -129,6 +147,28 @@ 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
eni := &ENI{
MacAddress: macAddr,
}

eniLinkName := eni.GetLinkName()
assert.EqualValues(t, linkName, eniLinkName)
}

// TestGetLinkNameFailure tests the retrieval of ENI Name in case of failure.
func TestGetLinkNameFailure(t *testing.T) {
netInterfaces = invalidNetInterfacesFunc
eni := &ENI{
MacAddress: macAddr,
}

eniLinkName := eni.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())
Expand Down
77 changes: 11 additions & 66 deletions agent/api/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import (
"github.com/aws/amazon-ecs-agent/agent/credentials"
"github.com/aws/amazon-ecs-agent/agent/dockerclient"
"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
"github.com/aws/amazon-ecs-agent/agent/ecscni"
"github.com/aws/amazon-ecs-agent/agent/taskresource"
"github.com/aws/amazon-ecs-agent/agent/taskresource/asmauth"
"github.com/aws/amazon-ecs-agent/agent/taskresource/asmsecret"
Expand All @@ -54,7 +53,6 @@ import (
"github.com/aws/amazon-ecs-agent/agent/utils"
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
"github.com/cihub/seelog"
"github.com/containernetworking/cni/libcni"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -1144,70 +1142,6 @@ func (task *Task) AddFirelensContainerBindMounts(firelensConfig *apicontainer.Fi
return nil
}

// BuildCNIConfig builds a list of CNI network configurations for the task.
// If includeIPAMConfig is set to true, the list also includes the bridge IPAM configuration.
func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) {
if !task.IsNetworkModeAWSVPC() {
return nil, errors.New("task config: task network mode is not AWSVPC")
}

var netconf *libcni.NetworkConfig
var ifName string
var err error

// Build a CNI network configuration for each ENI.
for _, eni := range task.ENIs {
switch eni.InterfaceAssociationProtocol {
// If the association protocol is set to "default" or unset (to preserve backwards
// compatibility), consider it a "standard" ENI attachment.
case "", apieni.DefaultInterfaceAssociationProtocol:
cniConfig.ID = eni.MacAddress
ifName, netconf, err = ecscni.NewENINetworkConfig(eni, cniConfig)
case apieni.VLANInterfaceAssociationProtocol:
cniConfig.ID = eni.MacAddress
ifName, netconf, err = ecscni.NewBranchENINetworkConfig(eni, cniConfig)
default:
err = errors.Errorf("task config: unknown interface association type: %s",
eni.InterfaceAssociationProtocol)
}

if err != nil {
return nil, err
}

cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})
}

// Build the bridge CNI network configuration.
// All AWSVPC tasks have a bridge network.
ifName, netconf, err = ecscni.NewBridgeNetworkConfig(cniConfig, includeIPAMConfig)
if err != nil {
return nil, err
}
cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})

// Build a CNI network configuration for AppMesh if enabled.
appMeshConfig := task.GetAppMesh()
if appMeshConfig != nil {
ifName, netconf, err = ecscni.NewAppMeshConfig(appMeshConfig, cniConfig)
if err != nil {
return nil, err
}
cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})
}

return cniConfig, nil
}

// IsNetworkModeAWSVPC checks if the task is configured to use the AWSVPC task networking feature.
func (task *Task) IsNetworkModeAWSVPC() bool {
return len(task.ENIs) > 0
Expand Down Expand Up @@ -2707,3 +2641,14 @@ func (task *Task) SetLocalIPAddress(addr string) {

task.LocalIPAddressUnsafe = addr
}

// UpdateTaskENIsLinkName updates the link name of all the enis associated with the task.
func (task *Task) UpdateTaskENIsLinkName() {
task.lock.Lock()
defer task.lock.Unlock()

// Update the link name of the task eni.
for _, eni := range task.ENIs {
eni.GetLinkName()
}
}
70 changes: 70 additions & 0 deletions agent/api/task/task_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@
package task

import (
"fmt"
"path/filepath"
"time"

apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status"
apieni "github.com/aws/amazon-ecs-agent/agent/api/eni"
"github.com/aws/amazon-ecs-agent/agent/config"
"github.com/aws/amazon-ecs-agent/agent/credentials"
"github.com/aws/amazon-ecs-agent/agent/ecscni"
"github.com/aws/amazon-ecs-agent/agent/taskresource"
"github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup"
resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status"
resourcetype "github.com/aws/amazon-ecs-agent/agent/taskresource/types"
"github.com/cihub/seelog"
"github.com/containernetworking/cni/libcni"
dockercontainer "github.com/docker/docker/api/types/container"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
Expand Down Expand Up @@ -251,3 +255,69 @@ func (task *Task) initializeFSxWindowsFileServerResource(cfg *config.Config, cre
resourceFields *taskresource.ResourceFields) error {
return errors.New("task with FSx for Windows File Server volumes is only supported on Windows container instance")
}

// BuildCNIConfig builds a list of CNI network configurations for the task.
// If includeIPAMConfig is set to true, the list also includes the bridge IPAM configuration.
func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) {
if !task.IsNetworkModeAWSVPC() {
return nil, errors.New("task config: task network mode is not AWSVPC")
}

var netconf *libcni.NetworkConfig
var ifName string
var err error

// Build a CNI network configuration for each ENI.
for _, eni := range task.ENIs {
switch eni.InterfaceAssociationProtocol {
// If the association protocol is set to "default" or unset (to preserve backwards
// compatibility), consider it a "standard" ENI attachment.
case "", apieni.DefaultInterfaceAssociationProtocol:
cniConfig.ID = eni.MacAddress
ifName, netconf, err = ecscni.NewENINetworkConfig(eni, cniConfig)
case apieni.VLANInterfaceAssociationProtocol:
cniConfig.ID = eni.MacAddress
ifName, netconf, err = ecscni.NewBranchENINetworkConfig(eni, cniConfig)
default:
err = errors.Errorf("task config: unknown interface association type: %s",
eni.InterfaceAssociationProtocol)
}

if err != nil {
return nil, err
}

cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})
}

// Build the bridge CNI network configuration.
// All AWSVPC tasks have a bridge network.
ifName, netconf, err = ecscni.NewBridgeNetworkConfig(cniConfig, includeIPAMConfig)
if err != nil {
return nil, err
}
cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})

// Build a CNI network configuration for AppMesh if enabled.
appMeshConfig := task.GetAppMesh()
if appMeshConfig != nil {
ifName, netconf, err = ecscni.NewAppMeshConfig(appMeshConfig, cniConfig)
if err != nil {
return nil, err
}
cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{
IfName: ifName,
CNINetworkConfig: netconf,
})
}

cniConfig.ContainerNetNS = fmt.Sprintf(ecscni.NetnsFormat, cniConfig.ContainerPID)

return cniConfig, nil
}
Loading

0 comments on commit d952d4c

Please sign in to comment.