ecs-agent/netlib/platform/common_linux.go (624 lines of code) (raw):
//go:build !windows
// +build !windows
// 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 platform
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs"
"github.com/aws/amazon-ecs-agent/ecs-agent/ec2"
"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
netlibdata "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/data"
"github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/appmesh"
"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/serviceconnect"
"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/utils/ioutilwrapper"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/netlinkwrapper"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/netwrapper"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/oswrapper"
"github.com/aws/amazon-ecs-agent/ecs-agent/volume"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
cnitypes "github.com/containernetworking/cni/pkg/types/100"
cnins "github.com/containernetworking/plugins/pkg/ns"
"github.com/pkg/errors"
)
const (
networkConfigFileDirectory = "/etc/netns"
networkConfigHostnameFilePath = "/etc/hostname"
networkConfigFileMode = 0644
taskDNSConfigFileMode = 0666
HostsLocalhostEntryIPv4 = "127.0.0.1 localhost"
HostsLocalhostEntryIPv6 = "::1 localhost"
// DNS related configuration.
HostnameFileName = "hostname"
ResolveConfFileName = "resolv.conf"
HostsFileName = "hosts"
// 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
dnsVolumeAccessor volume.TaskVolumeAccessor
os oswrapper.OS
ioutil ioutilwrapper.IOUtil
netlink netlinkwrapper.NetLink
stateDBDir string
cniClient ecscni.CNI
net netwrapper.Net
resolvConfPath string
}
// NewPlatform creates an implementation of the platform API depending on the
// platform type where the agent is executing.
func NewPlatform(
platformConfig Config,
volumeAccessor volume.TaskVolumeAccessor,
stateDBDirectory string,
netWrapper netwrapper.Net,
) (API, error) {
commonPlatform := common{
nsUtil: ecscni.NewNetNSUtil(),
dnsVolumeAccessor: volumeAccessor,
os: oswrapper.NewOS(),
ioutil: ioutilwrapper.NewIOUtil(),
netlink: netlinkwrapper.New(),
stateDBDir: stateDBDirectory,
cniClient: ecscni.NewCNIClient([]string{CNIPluginPathDefault}),
net: netWrapper,
resolvConfPath: platformConfig.ResolvConfPath,
}
switch platformConfig.Name {
case WarmpoolPlatform:
return &containerd{
common: commonPlatform,
}, nil
case FirecrackerPlatform:
return &firecraker{
common: commonPlatform,
}, nil
case WarmpoolDebugPlatform:
return &containerdDebug{
containerd: containerd{
common: commonPlatform,
},
}, nil
case FirecrackerDebugPlatform:
return &firecrackerDebug{
firecraker: firecraker{
common: commonPlatform,
},
}, nil
case ManagedPlatform, ManagedDebugPlatform:
ec2Client, err := ec2.NewEC2MetadataClient(nil)
if err != nil {
return nil, err
}
return &managedLinux{
common: commonPlatform,
client: ec2Client,
}, nil
}
return nil, errors.New("invalid platform: " + platformConfig.Name)
}
// 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,
singleNetNS bool,
ifaceToGuestNetNS map[string]string,
) (*tasknetworkconfig.TaskNetworkConfig, error) {
mode := types.NetworkMode(aws.ToString(taskPayload.NetworkMode))
var netNSs []*tasknetworkconfig.NetworkNamespace
var err error
switch mode {
case types.NetworkModeAwsvpc:
netNSs, err = c.buildAWSVPCNetworkNamespaces(taskID, taskPayload, singleNetNS, ifaceToGuestNetNS)
if err != nil {
return nil, errors.Wrap(err, "failed to translate network configuration")
}
case types.NetworkModeBridge:
return nil, errors.New("not implemented")
case types.NetworkModeHost:
return nil, errors.New("not implemented")
case types.NetworkModeNone:
return nil, errors.New("not implemented")
default:
return nil, errors.New("invalid network mode: " + string(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.
// 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).
// 4. Single netns, multiple interfaces (for V2N tasks on FoF).
func (c *common) buildAWSVPCNetworkNamespaces(
taskID string,
taskPayload *ecsacs.Task,
singleNetNS bool,
ifaceToGuestNetNS map[string]string,
) ([]*tasknetworkconfig.NetworkNamespace, error) {
if len(taskPayload.ElasticNetworkInterfaces) == 0 {
return nil, errors.New("interfaces list cannot be empty")
}
macToNames, err := c.interfacesMACToName()
if err != nil {
return nil, err
}
logger.Info("Building network configuration for awsvpc task", map[string]interface{}{
"SingleNetNS": singleNetNS,
"ENICount": len(taskPayload.ElasticNetworkInterfaces),
"HasContainerENIMapping": len(taskPayload.Containers[0].NetworkInterfaceNames) == 0,
})
// If we require all interfaces to be in one single netns, the network configuration is straight forward.
// This case is identified if the singleNetNS flag is set, or if the ENIs have an empty 'Name' field,
// or if there is only on ENI in the payload.
if singleNetNS || len(taskPayload.ElasticNetworkInterfaces) == 1 ||
aws.ToString(taskPayload.ElasticNetworkInterfaces[0].Name) == "" ||
len(taskPayload.Containers[0].NetworkInterfaceNames) == 0 {
primaryNetNS, err := c.buildNetNS(taskID,
0,
taskPayload.ElasticNetworkInterfaces,
taskPayload.ProxyConfiguration,
macToNames,
ifaceToGuestNetNS)
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.
// Order the containers such that the container attached to the interface with index 0 comes first.
sort.Slice(taskPayload.Containers, func(i, j int) bool {
iName := aws.ToString(taskPayload.Containers[i].NetworkInterfaceNames[0])
jName := aws.ToString(taskPayload.Containers[j].NetworkInterfaceNames[0])
return aws.ToInt64(ifNameMap[iName].Index) < aws.ToInt64(ifNameMap[jName].Index)
})
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.ToString(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.buildNetNS(taskID, nsIndex, ifaces, nil, macToNames, nil)
if err != nil {
return nil, err
}
netNSs = append(netNSs, netNS)
nsIndex += 1
}
return netNSs, nil
}
// buildNetNS creates a single network namespace object using the input network config data.
func (c *common) buildNetNS(
taskID string,
index int,
networkInterfaces []*ecsacs.ElasticNetworkInterface,
proxyConfig *ecsacs.ProxyConfiguration,
macToName map[string]string,
ifaceToGuestNetNS map[string]string,
) (*tasknetworkconfig.NetworkNamespace, error) {
var primaryIF *networkinterface.NetworkInterface
var ifaces []*networkinterface.NetworkInterface
lowestIdx := int64(indexHighValue)
for _, ni := range networkInterfaces {
guestNetNS := ifaceToGuestNetNS[aws.ToString(ni.Name)]
iface, err := networkinterface.New(ni, guestNetNS, networkInterfaces, macToName)
if err != nil {
return nil, err
}
if aws.ToInt64(ni.Index) < lowestIdx {
primaryIF = iface
lowestIdx = aws.ToInt64(ni.Index)
}
ifaces = append(ifaces, iface)
}
primaryIF.Default = true
netNSName := networkinterface.NetNSName(taskID, primaryIF.Name)
netNSPath := c.GetNetNSPath(netNSName)
logger.Info("Building network namespace model", map[string]interface{}{
"NetNSName": netNSName,
"NetNSPath": netNSPath,
})
return tasknetworkconfig.NewNetworkNamespace(
netNSName,
netNSPath,
index,
proxyConfig,
ifaces...)
}
// CreateNetNS creates a new network namespace with the specified path.
func (c *common) CreateNetNS(netNSPath string) error {
logger.Info("Creating network namespace", map[string]interface{}{
"NetNSPath": netNSPath,
})
nsExists, err := c.nsUtil.NSExists(netNSPath)
if err != nil {
return errors.Wrapf(err, "failed to check netns %s", netNSPath)
}
if nsExists {
return nil
}
err = c.nsUtil.NewNetNS(netNSPath)
if err != nil {
return errors.Wrapf(err, "failed to create netns %s", netNSPath)
}
// The loopback interface in a new network namespace is down by default in Linux.
// In case of a container launched using Docker, Docker itself ensures that loopback is up.
// Manually set the operational state to up to allow loopback communication.
err = c.nsUtil.ExecInNSPath(netNSPath, c.setUpLoFunc(netNSPath))
return err
}
func (c *common) DeleteNetNS(netNSPath string) error {
logger.Info("Deleting network namespace", map[string]interface{}{
"NetNSPath": netNSPath,
})
nsExists, err := c.nsUtil.NSExists(netNSPath)
if err != nil {
return errors.Wrapf(err, "failed to check netns %s", netNSPath)
}
if !nsExists {
return nil
}
err = c.nsUtil.DelNetNS(netNSPath)
if err != nil {
return errors.Wrapf(err, "failed to delete netns %s", netNSPath)
}
return nil
}
// DeleteDNSConfig deletes the directory at /etc/netns/<netns-name> and all its files.
func (c *common) DeleteDNSConfig(netNSName string) error {
logger.Info("Deleting DNS config", map[string]interface{}{
"NetNSName": netNSName,
})
if netNSName == "" {
return errors.New("netns name cannot be empty")
}
netNSDir := filepath.Join(networkConfigFileDirectory, netNSName)
_, err := c.os.Stat(netNSDir)
if c.os.IsNotExist(err) {
return errors.Wrap(err, "network config directory not found")
} else if err != nil {
return err
}
return c.os.RemoveAll(netNSDir)
}
// setUpLoFunc returns a method that sets the loop back interface inside a
// particular network namespace to the state "UP". This function is used to
// set up the loop back interface inside a task network namespace soon after
// its creation.
func (c *common) setUpLoFunc(netNSPath string) func(cnins.NetNS) error {
return func(cnins.NetNS) error {
// Get a handle to the loop back interface.
link, err := c.netlink.LinkByName("lo")
if err != nil {
return errors.Wrapf(err, "failed to find loopback interface in %s", netNSPath)
}
// Bring up the interface (ip link set dev lo up).
err = c.netlink.LinkSetUp(link)
if err != nil {
return errors.Wrapf(err, "failed to bring up loopback interface in %s", netNSPath)
}
return nil
}
}
// createDNSConfig creates the DNS config files for a particular network namespace.
// If the agent is running on debug mode, it reuses the host's DNS config files.
// If not, it gathers the DNS related data from the netns primary interface.
// The DNS files are written to the network namespace dir "/etc/netns/<netns-name>/".
// Afterward, these files are copied into the task volume to be bind-mounted into
// task containers.
func (c *common) createDNSConfig(
taskID string,
reuseHostDNSConfig bool,
netNS *tasknetworkconfig.NetworkNamespace) error {
logger.Info("Creating DNS config", map[string]interface{}{
"NetNSPath": netNS.Path,
"ReuseHostDNSConfig": reuseHostDNSConfig,
})
// For debug mode, resolv.conf and hosts files are same as the host machine.
// But for non debug mode they are all created using the data available in the ENI.
primaryIF := netNS.GetPrimaryInterface()
if primaryIF == nil {
return errors.New("unable to find primary interface")
}
if reuseHostDNSConfig {
if err := c.generateNetworkConfigFilesForDebugPlatforms(netNS.Name, primaryIF); err != nil {
return errors.Wrap(err, "unable to copy dns config files")
}
} else {
if err := c.createNetworkConfigFiles(netNS.Name, primaryIF); err != nil {
return errors.Wrap(err, "unable to create dns config file")
}
}
// Next, copy these files into a task volume, which can be used by containers as well, to
// configure their network.
if err := c.copyNetworkConfigFilesToTask(taskID, netNS.Name); err != nil {
return err
}
return nil
}
// createNetworkConfigFiles gathers DNS config information from a network interface
// object and writes them into the following files:
// 1. /etc/netns/<netNSName>/resolv.conf
// 2. /etc/netns/<netNSName>/hostname
// 3. /etc/netns/<netNSName>/hosts
func (c *common) createNetworkConfigFiles(netNSName string, primaryIF *networkinterface.NetworkInterface) error {
logger.Info("Creating DNS config files in netns directory", map[string]interface{}{
"NetNSName": netNSName,
})
// Create the dns configuration file directory.
_, err := c.os.Stat(filepath.Join(networkConfigFileDirectory, netNSName))
if err != nil && c.os.IsNotExist(err) {
err = c.os.MkdirAll(
filepath.Join(networkConfigFileDirectory, netNSName),
networkConfigFileMode)
}
if err != nil {
return errors.Wrap(err, "unable to create the dns config directory")
}
err = c.createResolvConfigFile(netNSName, primaryIF)
if err != nil {
return errors.Wrap(err, "unable to create resolv conf for netns")
}
err = c.createHostnameFileForNetNS(netNSName, primaryIF)
if err != nil {
return errors.Wrap(err, "unable to create hostname file for netns")
}
err = c.createHostnameFileForDefaultNetNS()
if err != nil {
return errors.Wrap(err, "unable to verify the existence of /etc/hostname on the host")
}
err = c.createHostsFile(netNSName, primaryIF)
if err != nil {
return errors.Wrap(err, "unable to create hosts file for netns")
}
return nil
}
// copyNetworkConfigFilesToTask copies the contents of the DNS config files for a
// task into the task volume.
func (c *common) copyNetworkConfigFilesToTask(taskID, netNSName string) error {
configFiles := []string{HostsFileName, ResolveConfFileName, HostnameFileName}
for _, file := range configFiles {
source := filepath.Join(networkConfigFileDirectory, netNSName, file)
err := c.dnsVolumeAccessor.CopyToVolume(taskID, source, file, networkConfigFileMode)
if err != nil {
return errors.Wrapf(err, "unable to populate %s for task", file)
}
}
return nil
}
// generateNetworkConfigFilesForDebugPlatforms generates network configuration files needed by containers
// when agent is running on a debug platform. In this case, instead of always creating new files, it just
// copies the relevant ones as required from the host.
func (c *common) generateNetworkConfigFilesForDebugPlatforms(
filesDirName string,
iface *networkinterface.NetworkInterface) error {
err := c.createHostnameFileForDefaultNetNS()
if err != nil {
return errors.Wrap(err, "unable to verify the existence of /etc/hostname on the host")
}
netNSDir := filepath.Join(networkConfigFileDirectory, filesDirName)
_, err = c.os.Stat(netNSDir)
if err != nil && c.os.IsNotExist(err) {
err = c.os.MkdirAll(netNSDir, networkConfigFileMode)
}
if err != nil {
return errors.Wrap(err, "unable to create the dns config directory")
}
err = c.createHostnameFileForNetNS(filesDirName, iface)
if err != nil {
return errors.Wrap(err, "unable to create hostname file for netns")
}
// Copy Host's resolv.conf file.
err = c.copyFile(filepath.Join(netNSDir, ResolveConfFileName),
filepath.Join(c.resolvConfPath, ResolveConfFileName),
taskDNSConfigFileMode)
if err != nil {
return err
}
err = c.copyFile(filepath.Join(netNSDir, HostsFileName), "/etc/hosts", taskDNSConfigFileMode)
if err != nil {
return err
}
return nil
}
func (c *common) copyFile(dst, src string, fileMode os.FileMode) error {
contents, err := c.ioutil.ReadFile(src)
if err != nil {
return errors.Wrapf(err, "unable to read %s", src)
}
err = c.ioutil.WriteFile(dst, contents, fileMode)
if err != nil {
return errors.Wrapf(err, "unable to write to %s", dst)
}
return nil
}
// createHostnameFileForNetNS creates the hostname file for the given network namespace.
func (c *common) createHostnameFileForNetNS(netConfigFilesDir string, iface *networkinterface.NetworkInterface) error {
logger.Info("Creating hostname file", map[string]interface{}{
"NetConfigFilesDir": netConfigFilesDir,
})
// \n is used as line separater for hosts file. Therefore we add \n at the end.
// Ref: https://github.com/moby/libnetwork/blob/v0.5.6/resolvconf/resolvconf.go#L209-L237
hostname := fmt.Sprintf("%s\n", iface.GetHostname())
return c.ioutil.WriteFile(
filepath.Join(networkConfigFileDirectory, netConfigFilesDir, HostnameFileName),
[]byte(hostname),
networkConfigFileMode)
}
// createHostnameFileForDefaultNetNS creates the hostname file for the default namespace
// if required. "ip netns exec" emits an error message if it cannot find the /etc/hostname
// file on the host's filesystem. Depending on the AMI config, that file might sometimes
// be absent. This method creates an empty file in cases where the file cannot be found.
func (c *common) createHostnameFileForDefaultNetNS() error {
f, err := c.os.OpenFile(networkConfigHostnameFilePath, os.O_RDONLY|os.O_CREATE, networkConfigFileMode)
if err != nil {
return err
}
defer f.Close()
return nil
}
func (c *common) createResolvConfigFile(netConfigFilesDir string, iface *networkinterface.NetworkInterface) error {
logger.Info("Creating resolv.conf file", map[string]interface{}{
"NetConfigFilesDir": netConfigFilesDir,
})
data := c.nsUtil.BuildResolvConfig(iface.DomainNameServers, iface.DomainNameSearchList)
return c.ioutil.WriteFile(
filepath.Join(networkConfigFileDirectory, netConfigFilesDir, ResolveConfFileName),
[]byte(data),
networkConfigFileMode)
}
func (c *common) createHostsFile(netNSName string, iface *networkinterface.NetworkInterface) error {
logger.Info("Creating hosts file", map[string]interface{}{
"NetNSName": netNSName,
})
var contents bytes.Buffer
if iface.IPv6Only() {
fmt.Fprintf(&contents, "%s\n%s %s\n",
HostsLocalhostEntryIPv6, iface.GetPrimaryIPv6Address(), iface.GetHostname())
} else {
// \n is used as line separater for hosts file. Therefore we add \n at the end.
// Ref: https://github.com/moby/libnetwork/blob/v0.5.6/resolvconf/resolvconf.go#L209-L237
fmt.Fprintf(&contents, "%s\n%s %s\n",
HostsLocalhostEntryIPv4, iface.GetPrimaryIPv4Address(), iface.GetHostname())
}
// Add any additional DNS entries associated with the ENI. This is required
// for service connect enabled tasks.
for _, dnsMapping := range iface.DNSMappingList {
fmt.Fprintf(&contents, "%s %s\n", dnsMapping.Address, dnsMapping.Hostname)
}
return c.ioutil.WriteFile(
filepath.Join(networkConfigFileDirectory, netNSName, HostsFileName),
contents.Bytes(),
networkConfigFileMode)
}
// configureInterface initiates the workflow for setting up a network interface
// inside a network namespace.
func (c *common) configureInterface(
ctx context.Context,
netNSPath string,
iface *networkinterface.NetworkInterface,
netDAO netlibdata.NetworkDataClient,
) error {
var err error
switch iface.InterfaceAssociationProtocol {
case networkinterface.DefaultInterfaceAssociationProtocol:
err = c.configureRegularENI(ctx, netNSPath, iface)
case networkinterface.VLANInterfaceAssociationProtocol:
err = c.configureBranchENI(ctx, netNSPath, iface)
case networkinterface.V2NInterfaceAssociationProtocol:
err = c.configureGENEVEInterface(ctx, netNSPath, iface, netDAO)
case networkinterface.VETHInterfaceAssociationProtocol:
// Do nothing.
return nil
default:
err = errors.New("invalid interface association protocol " + iface.InterfaceAssociationProtocol)
}
return err
}
// configureRegularENI configures a network interface for an ENI.
func (c *common) configureRegularENI(ctx context.Context, netNSPath string, eni *networkinterface.NetworkInterface) error {
logger.Info("Configuring regular ENI", map[string]interface{}{
"ENIName": eni.Name,
"NetNSPath": netNSPath,
})
var cniNetConf []ecscni.PluginConfig
var add bool
var err error
c.os.Setenv(CNIPluginLogFileEnv, ecscni.PluginLogPath)
c.os.Setenv(IPAMDataPathEnv, filepath.Join(c.stateDBDir, IPAMDataFileName))
switch eni.DesiredStatus {
case status.NetworkReadyPull:
// The task metadata interface setup by bridge plugin is required only for the primary ENI.
if eni.IsPrimary() {
cniNetConf = append(cniNetConf, createBridgePluginConfig(netNSPath))
}
cniNetConf = append(cniNetConf, createENIPluginConfigs(netNSPath, eni))
add = true
case status.NetworkDeleted:
// Regular ENIs are used in single-use warmpool instances, so cleanup isn't necessary.
cniNetConf = nil
add = false
}
_, err = c.executeCNIPlugin(ctx, add, cniNetConf...)
if err != nil {
err = errors.Wrap(err, "failed to setup regular eni")
}
return err
}
// configureBranchENI configures a network interface for a branch ENI.
func (c *common) configureBranchENI(ctx context.Context, netNSPath string, eni *networkinterface.NetworkInterface) error {
logger.Info("Configuring branch ENI", map[string]interface{}{
"ENIName": eni.Name,
"NetNSPath": netNSPath,
})
// Set the path for the IPAM CNI local db to track assigned IPs.
// Default path is /data but in some linux distros (i.e.Amazon BottleRocket) the root volume is read-only.
c.os.Setenv(IPAMDataPathEnv, filepath.Join(c.stateDBDir, IPAMDataFileName))
var cniNetConf []ecscni.PluginConfig
var err error
add := true
// Generate CNI network configuration based on the ENI's desired state.
switch eni.DesiredStatus {
case status.NetworkReadyPull:
// Setup bridge to connect task network namespace to TMDS running in host's primary netns.
if eni.IsPrimary() {
cniNetConf = append(cniNetConf, createBridgePluginConfig(netNSPath))
}
// We block IMDS access in awsvpc tasks.
cniNetConf = append(cniNetConf, createBranchENIConfig(netNSPath, eni, VPCBranchENIInterfaceTypeVlan, blockInstanceMetadataDefault))
case status.NetworkDeleted:
cniNetConf = append(cniNetConf, createBranchENIConfig(netNSPath, eni, VPCBranchENIInterfaceTypeVlan, blockInstanceMetadataDefault))
add = false
}
_, err = c.executeCNIPlugin(ctx, add, cniNetConf...)
if err != nil {
err = errors.Wrap(err, "failed to setup branch eni")
}
return err
}
func (c *common) configureGENEVEInterface(
ctx context.Context,
netNSPath string,
iface *networkinterface.NetworkInterface,
netDAO netlibdata.NetworkDataClient,
) error {
logger.Info("Configuring GENEVE interface", map[string]interface{}{
"ENIName": iface.Name,
"NetNSPath": netNSPath,
})
var cniNetConf ecscni.PluginConfig
add := true
// Generate CNI network configuration based on the ENI's desired state.
switch iface.DesiredStatus {
case status.NetworkReadyPull:
// Assign destination port and set device name for V2N ENI.
if err := ecscni.SetV2NDstPortAndDeviceName(iface, netDAO); err != nil {
return err
}
cniNetConf = NewTunnelConfig(netNSPath, iface, VPCTunnelInterfaceTypeGeneve)
case status.NetworkReady:
cniNetConf = NewTunnelConfig(netNSPath, iface, VPCTunnelInterfaceTypeTap)
case status.NetworkDeleted:
cniNetConf = NewTunnelConfig(netNSPath, iface, VPCTunnelInterfaceTypeTap)
add = false
}
result, err := c.executeCNIPlugin(ctx, add, cniNetConf)
if err != nil {
return err
}
logger.Info("GENEVE interface configured", map[string]interface{}{
"ENIName": iface.Name,
"NetNSPath": netNSPath,
})
switch iface.DesiredStatus {
case status.NetworkReadyPull:
// Once the ENI configuration for the READY_PULL state is complete, we need to read the
// OS assigned MAC address of the GENEVE interface. This MAC address is then associated
// with the V2N ENI. This MAC address will get persisted to the database once the ENI
// manager's state transitions from NONE to READY_PULL.
//
// This MAC address will later be used to create the interface definition
// in the request for creating the MicroVM. The benefit of this approach is that the agent
// will be aware of the MAC address assigned to the TAP interface inside the MicroVM (the one
// connected to the GENEVE interface) and can use this info for the network setup inside
// the MicroVM at a later stage.
if len(result) == 0 {
return errors.New("eni pull configuration: empty result from network setup")
}
newResult, err := cnitypes.GetResult(*result[0])
if err != nil {
return err
}
if len(newResult.Interfaces) == 0 || newResult.Interfaces[0].Mac == "" {
return errors.New("eni pull configuration: no mac address assigned")
}
iface.MacAddress = newResult.Interfaces[0].Mac
case status.NetworkDeleted:
// Once the task is stopped and the GENEVE interface is deleted, we can release the port so
// that it can be re-used by another task.
vni := iface.TunnelProperties.ID
dstPort := iface.TunnelProperties.DestinationPort
if err = netDAO.ReleaseGeneveDstPort(dstPort, vni); err != nil {
return err
}
}
return nil
}
// configureAppMesh configures AppMesh in a network namespace.
// This is used by warmpool and debug-warmpool platforms.
func (c *common) configureAppMesh(
ctx context.Context,
netNSPath string,
cfg *appmesh.AppMesh,
) error {
logger.Info("Configuring AppMesh", map[string]interface{}{
"NetNSPath": netNSPath,
})
c.os.Setenv(CNIPluginLogFileEnv, ecscni.PluginLogPath)
defer c.os.Unsetenv(CNIPluginLogFileEnv)
cniNetConf := createAppMeshPluginConfig(netNSPath, cfg)
_, err := c.executeCNIPlugin(ctx, true, cniNetConf)
if err != nil {
err = errors.Wrapf(err, "failed to setup appmesh netconfig %s", cniNetConf.String())
}
return err
}
// configureServiceConnect configures the task network namespace with service connect
// specific iptables rules.
func (c *common) configureServiceConnect(
ctx context.Context,
netNSPath string,
taskENI *networkinterface.NetworkInterface,
scConfig *serviceconnect.ServiceConnectConfig,
) error {
logger.Info("Configuring ServiceConnect", map[string]interface{}{
"NetNSPath": netNSPath,
})
c.os.Setenv(CNIPluginLogFileEnv, ecscni.PluginLogPath)
defer c.os.Unsetenv(CNIPluginLogFileEnv)
cniConf := createServiceConnectCNIConfig(taskENI, netNSPath, scConfig)
_, err := c.executeCNIPlugin(ctx, true, cniConf)
if err != nil {
return errors.Wrapf(err, "failed to setup service connect CNI plugin %s", cniConf.String())
}
return nil
}