Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable ECS EC2 task networking for Windows tasks #2915

Merged
merged 33 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
acfe727
Awsvpc windows : Check if the pause image is loaded on the container …
rawahars Jul 6, 2020
3c014d1
Changes for ENI Watcher in Windows which is used for Interface monito…
Jun 29, 2020
e6dbffe
Windows ENI watcher changes : Added Mocks
Jun 29, 2020
6073364
Windows ENI Watcher changes : Added unit tests
Jun 29, 2020
5032739
Following changes were made :
Jun 29, 2020
7549bbc
Rebase conflict resolution till last commit
Mar 5, 2021
3f830f4
Changes to build platform specific CNI plugins
Jul 17, 2020
a868498
Integration of various components - windows awsvpc
Jul 2, 2020
9db9317
Rebase error resolve
Apr 23, 2021
4dd0815
Obtain Primary CIDR of the instance VPC which will be used for genera…
Apr 22, 2021
620e9ba
Added comments for the added config and changed name of constant Prim…
Apr 29, 2021
74930f9
Retrieving ENIs name on the instance.
May 4, 2021
b9ab809
Changes to use vpc-eni plugin for Windows task networking.
May 4, 2021
b01f3a1
Changed cni logfile path for each platform.
May 4, 2021
f98a7f5
Update task eni dependencies before launching a task.
May 6, 2021
dc6eed4
Injecting net.Interfaces mock for getting eni link name.
May 6, 2021
8fcbdf0
Retry network namespace setup if it fails.
May 6, 2021
d87205a
Added CNI plugin configuration for setting up ecs-bridge endpoint for…
May 7, 2021
666d19c
Increase network setup timeout for Windows and protect cleanupNS for …
May 10, 2021
959d227
Rectified scenario when second ENIByMac function call is not initiated.
May 12, 2021
b7ddcd8
Introduced a CNI guard interface which is used by Windows to lock the…
May 14, 2021
c338917
Invoke additional commands to setup the task network namespace.
May 12, 2021
eb161cf
Change DockerClientAPI StartContainerExec to accept runtime configura…
May 14, 2021
619b6c4
Minor modifications for improving code quality
May 17, 2021
7155d6f
Refactor namespace command execution and firewall creation methods to…
May 19, 2021
731d342
Encapsulating execCmdExecutorFn inside the helper struct.
May 20, 2021
e3dab98
EC2 task networking on Windows: gMSA support, adding additional local…
rawahars Jun 9, 2021
42eff13
EC2 task networking for Windows: Build vpc-eni CNI plugin for Windows…
rawahars Jun 10, 2021
de52ccf
Bug fix: Making endpoint name format consistent with vpc-eni plugin
Jun 11, 2021
f617e70
Removed redundant checks for DNS server information
Jun 17, 2021
2cc0bcd
Data validation for CNI Plugin invocation config
Jun 20, 2021
7676f38
Change in implementation design for blocking IMDS.
Jun 18, 2021
3495999
Added test for the case when IMDS is disabled for the task.
Jun 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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