agent/ecscni/plugin.go (145 lines of code) (raw):
// 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 ecscni
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"github.com/cihub/seelog"
"github.com/containernetworking/cni/libcni"
cniTypesCurrent "github.com/containernetworking/cni/pkg/types/100"
"github.com/pkg/errors"
"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
)
// CNIClient defines the method of setting/cleaning up container namespace
type CNIClient interface {
// Version returns the version of the plugin
Version(string) (string, error)
// Capabilities returns the capabilities supported by a plugin
Capabilities(string) ([]string, error)
// SetupNS sets up the namespace of container
SetupNS(context.Context, *Config, time.Duration) (*cniTypesCurrent.Result, error)
// CleanupNS cleans up the container namespace
CleanupNS(context.Context, *Config, time.Duration) error
// ReleaseIPResource marks the ip available in the ipam db
ReleaseIPResource(context.Context, *Config, time.Duration) error
}
// cniGuard is the mutex interface for CNI Client.
// It is actively used only on Windows due to limitations of hcsshim.
// It is used to serialize the cleanupNS calls when multiple tasks are stopped at the same time.
// During setupNS, we use retries for better task startup performance.
type cniGuard interface {
lock()
unlock()
}
// cniClient is the client to call plugin and setup the network
type cniClient struct {
pluginsPath string
libcni libcni.CNI
guard cniGuard
}
// guard is the client to call lock and unlock methods on the mutex.
type guard struct {
mutex *sync.Mutex
}
// NewClient creates a client of ecscni which is used to invoke the plugin
func NewClient(pluginsPath string) CNIClient {
libcniConfig := &libcni.CNIConfig{
Path: []string{pluginsPath},
}
cniClient := &cniClient{
pluginsPath: pluginsPath,
libcni: libcniConfig,
guard: newCNIGuard(),
}
cniClient.init()
return cniClient
}
func (client *cniClient) init() {
// Set environment variables for CNI plugins.
os.Setenv("ECS_CNI_LOGLEVEL", logger.GetLevel())
os.Setenv("VPC_CNI_LOG_LEVEL", logger.GetLevel())
os.Setenv("VPC_CNI_LOG_FILE", vpcCNIPluginPath)
}
// SetupNS sets up the network namespace of a task by invoking the given CNI network configurations.
// It returns the result of the bridge plugin invocation as that result is used to parse the IPv4
// address allocated to the veth device attached to the task by the task engine.
func (client *cniClient) SetupNS(
ctx context.Context,
cfg *Config,
timeout time.Duration) (*cniTypesCurrent.Result, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return client.setupNS(ctx, cfg)
}
// CleanupNS will clean up the container namespace, including remove the veth
// pair and stop the dhclient
func (client *cniClient) CleanupNS(
ctx context.Context,
cfg *Config,
timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return client.cleanupNS(ctx, cfg)
}
// cleanupNS is called by CleanupNS to cleanup the task namespace by invoking DEL for given CNI configurations
func (client *cniClient) cleanupNS(ctx context.Context, cfg *Config) error {
client.guard.lock()
defer client.guard.unlock()
seelog.Debugf("[ECSCNI] Cleaning up the container namespace %s", cfg.ContainerID)
runtimeConfig := libcni.RuntimeConf{
ContainerID: cfg.ContainerID,
NetNS: cfg.ContainerNetNS,
}
var delError error
// Execute all CNI network configurations serially, in the reverse order.
for i := len(cfg.NetworkConfigs) - 1; i >= 0; i-- {
networkConfig := cfg.NetworkConfigs[i]
cniNetworkConfig := networkConfig.CNINetworkConfig
seelog.Debugf("[ECSCNI] Deleting network %s type %s in the container namespace %s",
cniNetworkConfig.Network.Name,
cniNetworkConfig.Network.Type,
cfg.ContainerID)
runtimeConfig.IfName = networkConfig.IfName
err := client.libcni.DelNetwork(ctx, cniNetworkConfig, &runtimeConfig)
if err != nil {
// In case of error, continue cleanup as much as possible before conceding error.
seelog.Errorf("Delete network failed: %v", err)
delError = errors.Wrapf(err, "delete network failed")
}
seelog.Debugf("[ECSCNI] Completed deleting network %s type %s in the container namespace %s",
cniNetworkConfig.Network.Name,
cniNetworkConfig.Network.Type,
cfg.ContainerID)
}
seelog.Debugf("[ECSCNI] Completed cleaning up the container namespace %s", cfg.ContainerID)
return delError
}
// Version returns the version of the plugin
func (client *cniClient) Version(name string) (string, error) {
file := filepath.Join(client.pluginsPath, name)
// Check if the plugin file exists before executing it
_, err := os.Stat(file)
if err != nil {
return "", err
}
cmd := exec.Command(file, versionCommand)
versionInfo, err := cmd.Output()
if err != nil {
return "", err
}
version := &cniPluginVersion{}
// For Linux, versionInfo is of the format
// {"version":"2017.06.0","dirty":true,"gitShortHash":"226db36"}
// For Windows, it is of the format
// {"version":"2017.06.0","gitShortHash":"226db36","built":"2048-08-16T12:10:14-08:00"}
// Unmarshal this
err = json.Unmarshal(versionInfo, version)
if err != nil {
return "", errors.Wrapf(err, "ecscni: unmarshal version from string: %s", versionInfo)
}
return version.str(), nil
}
// Capabilities returns the capabilities supported by a plugin
func (client *cniClient) Capabilities(name string) ([]string, error) {
file := filepath.Join(client.pluginsPath, name)
// Check if the plugin file exists before executing it
_, err := os.Stat(file)
if err != nil {
return nil, errors.Wrapf(err, "ecscni: unable to describe file info for '%s'", file)
}
cmd := exec.Command(file, capabilitiesCommand)
capabilitiesInfo, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "ecscni: failed invoking capabilities command for '%s'", name)
}
capabilities := &struct {
Capabilities []string `json:"capabilities"`
}{}
err = json.Unmarshal(capabilitiesInfo, capabilities)
if err != nil {
return nil, errors.Wrapf(err, "ecscni: failed to unmarshal capabilities for '%s' from string: %s", name, capabilitiesInfo)
}
return capabilities.Capabilities, nil
}
func (cniGuard *guard) lock() {
if cniGuard.mutex != nil {
cniGuard.mutex.Lock()
}
}
func (cniGuard *guard) unlock() {
if cniGuard.mutex != nil {
cniGuard.mutex.Unlock()
}
}