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

[ECS Windows] ECS Exec feature for Windows workloads #3053

Merged
merged 14 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 13 additions & 22 deletions agent/app/agent_capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ const (
capabilityEnvFilesS3 = "env-files.s3"
capabilityFSxWindowsFileServer = "fsxWindowsFileServer"
capabilityExec = "execute-command"
capabilityDepsRootDir = "/managed-agents"
capabilityExecBinRelativePath = "bin"
capabilityExecConfigRelativePath = "config"
capabilityExecCertsRelativePath = "certs"
Expand Down Expand Up @@ -97,19 +96,12 @@ var (
// ecs agent version 1.39.0 supports bulk loading env vars through environmentFiles in S3
capabilityEnvFilesS3,
}
capabilityExecRequiredBinaries = []string{
"amazon-ssm-agent",
"ssm-agent-worker",
"ssm-session-worker",
}
capabilityExecRequiredCerts = []string{
"tls-ca-bundle.pem",
}
// use empty struct as value type to simulate set
capabilityExecInvalidSsmVersions = map[string]struct{}{}

pathExists = defaultPathExists
getSubDirectories = defaultGetSubDirectories
pathExists = defaultPathExists
getSubDirectories = defaultGetSubDirectories
isPlatformExecSupported = defaultIsPlatformExecSupported

// List of capabilities that are not supported on external capacity.
externalUnsupportedCapabilities = []string{
Expand All @@ -127,6 +119,10 @@ var (
externalSpecificCapabilities = []string{
attributePrefix + capabilityExternal,
}

capabilityExecRootDir = filepath.Join(capabilityDepsRootDir, capabilityExec)
binDir = filepath.Join(capabilityExecRootDir, capabilityExecBinRelativePath)
configDir = filepath.Join(capabilityExecRootDir, capabilityExecConfigRelativePath)
)

// capabilities returns the supported capabilities of this agent / docker-client pair.
Expand Down Expand Up @@ -384,20 +380,15 @@ func (agent *ecsAgent) appendTaskENICapabilities(capabilities []*ecs.Attribute)
}

func (agent *ecsAgent) appendExecCapabilities(capabilities []*ecs.Attribute) ([]*ecs.Attribute, error) {

// Only Windows 2019 and above are supported, all Linux supported
if platformSupported, err := isPlatformExecSupported(); err != nil || !platformSupported {
return capabilities, err
}

// for an instance to be exec-enabled, it needs resources needed by SSM (binaries, configuration files and certs)
// the following bind mounts are defined in ecs-init and added to the ecs-agent container

capabilityExecRootDir := filepath.Join(capabilityDepsRootDir, capabilityExec)
binDir := filepath.Join(capabilityExecRootDir, capabilityExecBinRelativePath)
configDir := filepath.Join(capabilityExecRootDir, capabilityExecConfigRelativePath)
certsDir := filepath.Join(capabilityExecRootDir, capabilityExecCertsRelativePath)

// top-level folders, /bin, /config, /certs
dependencies := map[string][]string{
binDir: []string{},
configDir: []string{},
certsDir: capabilityExecRequiredCerts,
}
if exists, err := dependenciesExist(dependencies); err != nil || !exists {
return capabilities, err
}
Expand Down
10 changes: 10 additions & 0 deletions agent/app/agent_capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,7 @@ func TestCapabilitiesExecuteCommand(t *testing.T) {
getSubDirectories func(path string) ([]string, error)
invalidSsmVersions map[string]struct{}
shouldHaveExecCapability bool
osPlatformNotSupported bool
}{
{
name: "execute-command capability should not be added if any required file is not found",
Expand Down Expand Up @@ -917,17 +918,26 @@ func TestCapabilitiesExecuteCommand(t *testing.T) {
getSubDirectories: func(path string) ([]string, error) { return []string{"3.0.236.0", "3.1.23.0"}, nil },
shouldHaveExecCapability: true,
},
{
name: "execute-command capability should not be added if os platform is not supported",
pathExists: func(path string, shouldBeDirectory bool) (bool, error) { return true, nil },
getSubDirectories: func(path string) ([]string, error) { return []string{"3.0.236.0"}, nil },
osPlatformNotSupported: true,
shouldHaveExecCapability: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pathExists = tc.pathExists
getSubDirectories = tc.getSubDirectories
oCapabilityExecInvalidSsmVersions := capabilityExecInvalidSsmVersions
capabilityExecInvalidSsmVersions = tc.invalidSsmVersions
isPlatformExecSupported = func() (bool, error) { return !tc.osPlatformNotSupported, nil }
defer func() {
mockPathExists(false)
getSubDirectories = defaultGetSubDirectories
capabilityExecInvalidSsmVersions = oCapabilityExecInvalidSsmVersions
isPlatformExecSupported = defaultIsPlatformExecSupported
}()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
35 changes: 30 additions & 5 deletions agent/app/agent_capability_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"path/filepath"
"strings"

"github.com/aws/amazon-ecs-agent/agent/config"
Expand All @@ -30,11 +31,31 @@ import (
)

const (
AVX = "avx"
AVX2 = "avx2"
SSE41 = "sse4_1"
SSE42 = "sse4_2"
CpuInfoPath = "/proc/cpuinfo"
AVX = "avx"
AVX2 = "avx2"
SSE41 = "sse4_1"
SSE42 = "sse4_2"
CpuInfoPath = "/proc/cpuinfo"
capabilityDepsRootDir = "/managed-agents"
)

var (
certsDir = filepath.Join(capabilityExecRootDir, capabilityExecCertsRelativePath)
capabilityExecRequiredCerts = []string{
"tls-ca-bundle.pem",
}
capabilityExecRequiredBinaries = []string{
"amazon-ssm-agent",
"ssm-agent-worker",
"ssm-session-worker",
}

// top-level folders, /bin, /config, /certs
dependencies = map[string][]string{
binDir: []string{},
configDir: []string{},
certsDir: capabilityExecRequiredCerts,
}
)

func (agent *ecsAgent) appendVolumeDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
Expand Down Expand Up @@ -210,3 +231,7 @@ func (agent *ecsAgent) getTaskENIPluginVersionAttribute() (*ecs.Attribute, error
Value: aws.String(version),
}, nil
}

func defaultIsPlatformExecSupported() (bool, error) {
return true, nil
}
13 changes: 13 additions & 0 deletions agent/app/agent_capability_unspecified.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ import (
"github.com/cihub/seelog"
)

const (
capabilityDepsRootDir = ""
)

var (
capabilityExecRequiredBinaries = []string{}
dependencies = map[string][]string{}
)

func (agent *ecsAgent) appendVolumeDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
// "local" is default docker driver
capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+volume.DockerLocalVolumeDriver)
Expand Down Expand Up @@ -131,3 +140,7 @@ func (agent *ecsAgent) appendFSxWindowsFileServerCapabilities(capabilities []*ec
func (agent *ecsAgent) getTaskENIPluginVersionAttribute() (*ecs.Attribute, error) {
return nil, errors.New("unsupported platform")
}

func defaultIsPlatformExecSupported() (bool, error) {
return false, nil
}
36 changes: 36 additions & 0 deletions agent/app/agent_capability_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,40 @@
package app

import (
"path/filepath"

"github.com/aws/amazon-ecs-agent/agent/config"
"github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs"
"github.com/aws/amazon-ecs-agent/agent/ecscni"
"github.com/aws/amazon-ecs-agent/agent/taskresource/volume"
"github.com/aws/aws-sdk-go/aws"
"github.com/cihub/seelog"
)

var (
capabilityDepsRootDir = filepath.Join(config.AmazonECSProgramFiles, "managed-agents")
ssmPluginDir = filepath.Join(config.AmazonProgramFiles, "SSM", "Plugins")
sessionManagerShellDir = filepath.Join(ssmPluginDir, "SessionManagerShell")
awsCloudWatchDir = filepath.Join(ssmPluginDir, "awsCloudWatch")
awsDomainJoin = filepath.Join(ssmPluginDir, "awsDomainJoin")

capabilityExecRequiredBinaries = []string{
"amazon-ssm-agent.exe",
"ssm-agent-worker.exe",
"ssm-session-worker.exe",
}

// top-level folders, /bin, /config, /plugins
dependencies = map[string][]string{
binDir: []string{},
configDir: []string{},
ssmPluginDir: []string{},
sessionManagerShellDir: []string{},
awsCloudWatchDir: []string{},
awsDomainJoin: []string{},
}
)

func (agent *ecsAgent) appendVolumeDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
// "local" is default docker driver
return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+volume.DockerLocalVolumeDriver)
Expand Down Expand Up @@ -113,3 +140,12 @@ func (agent *ecsAgent) getTaskENIPluginVersionAttribute() (*ecs.Attribute, error
Value: aws.String(version),
}, nil
}

var isWindows2016 = config.IsWindows2016

func defaultIsPlatformExecSupported() (bool, error) {
if windows2016, err := isWindows2016(); err != nil || windows2016 {
return false, err
}
return true, nil
}
60 changes: 60 additions & 0 deletions agent/app/agent_capability_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,63 @@ func TestAppendFSxWindowsFileServerCapabilitiesFalse(t *testing.T) {

assert.Equal(t, len(expectedCapabilities), len(capabilities))
}

func TestAppendExecCapabilities(t *testing.T) {
var inputCapabilities []*ecs.Attribute
var expectedCapabilities []*ecs.Attribute
execCapability := ecs.Attribute{
Name: aws.String(attributePrefix + capabilityExec),
}

expectedCapabilities = append(expectedCapabilities,
[]*ecs.Attribute{}...)
testCases := []struct {
name string
pathExists func(string, bool) (bool, error)
getSubDirectories func(path string) ([]string, error)
isWindows2016Instance bool
shouldHaveExecCapability bool
}{
{
name: "execute-command capability should not be added on Win2016 instances",
pathExists: func(path string, shouldBeDirectory bool) (bool, error) { return true, nil },
getSubDirectories: func(path string) ([]string, error) { return []string{"3.0.236.0"}, nil },
isWindows2016Instance: true,
shouldHaveExecCapability: false,
},
{
name: "execute-command capability should be added if not a Win2016 instances",
pathExists: func(path string, shouldBeDirectory bool) (bool, error) { return true, nil },
getSubDirectories: func(path string) ([]string, error) { return []string{"3.0.236.0"}, nil },
isWindows2016Instance: false,
shouldHaveExecCapability: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
isWindows2016 = func() (bool, error) { return tc.isWindows2016Instance, nil }
pathExists = tc.pathExists
getSubDirectories = tc.getSubDirectories

defer func() {
isWindows2016 = config.IsWindows2016
pathExists = defaultPathExists
getSubDirectories = defaultGetSubDirectories
}()
agent := &ecsAgent{
cfg: &config.Config{},
}

capabilities, err := agent.appendExecCapabilities(inputCapabilities)

assert.NoError(t, err)

if tc.shouldHaveExecCapability {
assert.Contains(t, capabilities, &execCapability)
} else {
assert.NotContains(t, capabilities, &execCapability)
}
})
}
}
11 changes: 11 additions & 0 deletions agent/config/config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ const (
defaultCNIPluginDirName = "cni"
)

var (
envProgramFiles = utils.DefaultIfBlank(os.Getenv("ProgramFiles"), `C:\Program Files`)
envProgramData = utils.DefaultIfBlank(os.Getenv("ProgramData"), `C:\ProgramData`)

AmazonProgramFiles = filepath.Join(envProgramFiles, "Amazon")
AmazonProgramData = filepath.Join(envProgramData, "Amazon")

AmazonECSProgramFiles = filepath.Join(envProgramFiles, "Amazon", "ECS")
AmazonECSProgramData = filepath.Join(AmazonProgramData, "ECS")
)

// DefaultConfig returns the default configuration for Windows
func DefaultConfig() Config {
programData := utils.DefaultIfBlank(os.Getenv("ProgramData"), `C:\ProgramData`)
Expand Down
Loading