diff --git a/.gitignore b/.gitignore index bcf7e98552e..f2a7dc839da 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ out/ coverage/ _bin/ *.exe +*.exe~ *.zip *.swp *.orig @@ -15,3 +16,4 @@ _bin/ /misc/windows-iam/devcon* /misc/pause-container/pause *-stamp +/.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 876e2d0cdd6..3353ace6f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## Unreleased * Feature - Support pulling from Amazon ECR with specified IAM role in task definition * Feature - Enable support for task level CPU and memory constraints. +* Feature - Enable the ECS agent to run as a Windows service. [#1070](https://github.com/aws/amazon-ecs-agent/pull/1070) +* Enhancement - Support CloudWatch metrics for Windows. [#1077](https://github.com/aws/amazon-ecs-agent/pull/1077) +* Enhancement - Enforce memory limits on Windows. [#1069](https://github.com/aws/amazon-ecs-agent/pull/1069) +* Enhancement - Enforce CPU limits on Windows. [#1089](https://github.com/aws/amazon-ecs-agent/pull/1089) +* Enhancement - Simplify task IAM credential host setup. [#1105](https://github.com/aws/amazon-ecs-agent/pull/1105) ## 1.15.2 * Bug - Fixed a bug where container state information wasn't reported. [#1076](https://github.com/aws/amazon-ecs-agent/pull/1076) @@ -11,6 +16,7 @@ * Bug - Fixed a bug where container state information wasn't reported. [#1067](https://github.com/aws/amazon-ecs-agent/pull/1067) * Bug - Fixed a bug where a task can be blocked in creating state. [#1048](https://github.com/aws/amazon-ecs-agent/pull/1048) * Bug - Fixed dynamic HostPort in container metadata. [#1052](https://github.com/aws/amazon-ecs-agent/pull/1052) +* Bug - Fixed bug on Windows where container memory limits are not enforced. [#1069](https://github.com/aws/amazon-ecs-agent/pull/1069) ## 1.15.0 * Feature - Support for provisioning tasks with ENIs. diff --git a/README.md b/README.md index ce9dab94db5..78374f81714 100644 --- a/README.md +++ b/README.md @@ -48,26 +48,72 @@ See also the Advanced Usage section below. ### On Windows Server 2016 -On Windows Server 2016, the Amazon ECS Container Agent runs as a process on the -host. Unlike Linux, the agent may not run inside a container as it uses the -host's registry and the named pipe at `\\.\pipe\docker_engine` to communicate -with the Docker daemon. +On Windows Server 2016, the Amazon ECS Container Agent runs as a process or +service on the host. Unlike Linux, the agent may not run inside a container as +it uses the host's registry and the named pipe at `\\.\pipe\docker_engine` to +communicate with the Docker daemon. + +#### As a Service +To install the service, you can do the following: + +```powershell +PS C:\> # Set up directories the agent uses +PS C:\> New-Item -Type directory -Path ${env:ProgramFiles}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS\data -Force +PS C:\> # Set up configuration +PS C:\> $ecsExeDir = "${env:ProgramFiles}\Amazon\ECS" +PS C:\> [Environment]::SetEnvironmentVariable("ECS_CLUSTER", "my-windows-cluster", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "${env:ProgramData}\Amazon\ECS\log\ecs-agent.log", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "${env:ProgramData}\Amazon\ECS\data", "Machine") +PS C:\> # Download the agent +PS C:\> $agentVersion = "latest" +PS C:\> $agentZipUri = "https://s3.amazonaws.com/amazon-ecs-agent/ecs-agent-windows-$agentVersion.zip" +PS C:\> $zipFile = "${env:TEMP}\ecs-agent.zip" +PS C:\> Invoke-RestMethod -OutFile $zipFile -Uri $agentZipUri +PS C:\> # Put the executables in the executable directory. +PS C:\> Expand-Archive -Path $zipFile -DestinationPath $ecsExeDir -Force +PS C:\> Set-Location ${ecsExeDir} +PS C:\> # Set $EnableTaskIAMRoles to $true to enable task IAM roles +PS C:\> # Note that enabling IAM roles will make port 80 unavailable for tasks. +PS C:\> [bool]$EnableTaskIAMRoles = $false +PS C:\> if (${EnableTaskIAMRoles} { +>> .\hostsetup.ps1 +>> } +PS C:\> # Install the agent service +PS C:\> New-Service -Name "AmazonECS" ` + -BinaryPathName "$ecsExeDir\amazon-ecs-agent.exe -windows-service" ` + -DisplayName "Amazon ECS" ` + -Description "Amazon ECS service runs the Amazon ECS agent" ` + -DependsOn Docker ` + -StartupType Manual +PS C:\> sc.exe failure AmazonECS reset=300 actions=restart/5000/restart/30000/restart/60000 +PS C:\> sc.exe failureflag AmazonECS 1 +``` + +To run the service, you can do the following: +```powershell +Start-Service AmazonECS +``` + +#### As a Process ```powershell PS C:\> # Set up directories the agent uses -PS C:\> New-Item -Type directory -Path $ProgramFiles\Amazon\ECS -PS C:\> New-Item -Type directory -Path $ProgramData\Amazon\ECS +PS C:\> New-Item -Type directory -Path ${env:ProgramFiles}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS\data -Force PS C:\> # Set up configuration -PS C:\> $ecsExeDir = "$env:ProgramFiles\Amazon\ECS" +PS C:\> $ecsExeDir = "${env:ProgramFiles}\Amazon\ECS" PS C:\> [Environment]::SetEnvironmentVariable("ECS_CLUSTER", "my-windows-cluster", "Machine") -PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "$ProgramData\Amazon\ECS\log\ecs-agent.log", "Machine") -PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "$ProgramData\Amazon\ECS\data", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "${env:ProgramData}\Amazon\ECS\log\ecs-agent.log", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "${env:ProgramData}\Amazon\ECS\data", "Machine") PS C:\> # Set this environment variable to "true" to enable IAM roles. Note that enabling IAM roles will make port 80 unavailable for tasks. PS C:\> [Environment]::SetEnvironmentVariable("ECS_ENABLE_TASK_IAM_ROLE", "false", "Machine") PS C:\> # Download the agent PS C:\> $agentVersion = "latest" PS C:\> $agentZipUri = "https://s3.amazonaws.com/amazon-ecs-agent/ecs-agent-windows-$agentVersion.zip" -PS C:\> $zipFile = "$env:TEMP\ecs-agent.zip" +PS C:\> $zipFile = "${env:TEMP}\ecs-agent.zip" PS C:\> Invoke-RestMethod -OutFile $zipFile -Uri $agentZipUri PS C:\> # Put the executables in the executable directory. PS C:\> Expand-Archive -Path $zipFile -DestinationPath $ecsExeDir -Force diff --git a/agent/Gopkg.lock b/agent/Gopkg.lock index 80f5afbcc56..a863a23c791 100644 --- a/agent/Gopkg.lock +++ b/agent/Gopkg.lock @@ -196,7 +196,7 @@ [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix","windows","windows/registry"] + packages = ["unix","windows","windows/registry","windows/svc","windows/svc/eventlog"] revision = "bf42f188b9bc6f2cf5b8ee5a912ef1aedd0eba4c" [[projects]] @@ -208,6 +208,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7e9609e3a34b188626a65c0e59b6f3ab4b5df26ee3cebb379ea69f5120ba8695" + inputs-digest = "93e82296f28adc2d10fe4fe2cf05b6a786a2c87d9d311b31f28e29a9e8e81da7" solver-name = "gps-cdcl" solver-version = 1 diff --git a/agent/api/container.go b/agent/api/container.go index 40639facd01..aea4ea62e14 100644 --- a/agent/api/container.go +++ b/agent/api/container.go @@ -22,9 +22,6 @@ import ( ) const ( - // DockerContainerMinimumMemoryInBytes is the minimum amount of - // memory to be allocated to a docker container - DockerContainerMinimumMemoryInBytes = 4 * 1024 * 1024 // 4MB // defaultContainerSteadyStateStatus defines the container status at // which the container is assumed to be in steady state. It is set // to 'ContainerRunning' unless overridden diff --git a/agent/api/container_unix.go b/agent/api/container_unix.go new file mode 100644 index 00000000000..8541651fbc7 --- /dev/null +++ b/agent/api/container_unix.go @@ -0,0 +1,22 @@ +// +build !windows + +// Copyright 2014-2017 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 api + +const ( + // DockerContainerMinimumMemoryInBytes is the minimum amount of + // memory to be allocated to a docker container + DockerContainerMinimumMemoryInBytes = 4 * 1024 * 1024 // 4MB +) diff --git a/agent/api/container_windows.go b/agent/api/container_windows.go new file mode 100644 index 00000000000..38e082f06fd --- /dev/null +++ b/agent/api/container_windows.go @@ -0,0 +1,22 @@ +// +build windows + +// Copyright 2014-2017 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 api + +const ( + // DockerContainerMinimumMemoryInBytes is the minimum amount of + // memory to be allocated to a docker container + DockerContainerMinimumMemoryInBytes = 256 * 1024 * 1024 // 256MB +) diff --git a/agent/api/task.go b/agent/api/task.go index 5359d60c243..bf55b359d62 100644 --- a/agent/api/task.go +++ b/agent/api/task.go @@ -26,6 +26,7 @@ import ( "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/engine/dockerclient" "github.com/aws/amazon-ecs-agent/agent/engine/emptyvolume" "github.com/aws/amazon-ecs-agent/agent/utils/ttime" "github.com/aws/aws-sdk-go/aws/arn" @@ -414,11 +415,11 @@ func (task *Task) getEarliestKnownTaskStatusForContainers() TaskStatus { // DockerConfig converts the given container in this task to the format of // GoDockerClient's 'Config' struct -func (task *Task) DockerConfig(container *Container) (*docker.Config, *DockerClientConfigError) { - return task.dockerConfig(container) +func (task *Task) DockerConfig(container *Container, apiVersion dockerclient.DockerVersion) (*docker.Config, *DockerClientConfigError) { + return task.dockerConfig(container, apiVersion) } -func (task *Task) dockerConfig(container *Container) (*docker.Config, *DockerClientConfigError) { +func (task *Task) dockerConfig(container *Container, apiVersion dockerclient.DockerVersion) (*docker.Config, *DockerClientConfigError) { dockerVolumes, err := task.dockerConfigVolumes(container) if err != nil { return nil, &DockerClientConfigError{err.Error()} @@ -447,8 +448,11 @@ func (task *Task) dockerConfig(container *Container) (*docker.Config, *DockerCli ExposedPorts: task.dockerExposedPorts(container), Volumes: dockerVolumes, Env: dockerEnv, - Memory: dockerMem, - CPUShares: task.dockerCPUShares(container.CPU), + } + + err = task.SetConfigHostconfigBasedOnVersion(container, config, nil, apiVersion) + if err != nil { + return nil, &DockerClientConfigError{"setting docker config failed, err: " + err.Error()} } if container.DockerConfig.Config != nil { @@ -464,6 +468,43 @@ func (task *Task) dockerConfig(container *Container) (*docker.Config, *DockerCli return config, nil } +// SetConfigHostconfigBasedOnVersion sets the fields in both Config and HostConfig based on api version for backward compatibility +func (task *Task) SetConfigHostconfigBasedOnVersion(container *Container, config *docker.Config, hc *docker.HostConfig, apiVersion dockerclient.DockerVersion) error { + // Convert MB to B + dockerMem := int64(container.Memory * 1024 * 1024) + if dockerMem != 0 && dockerMem < DockerContainerMinimumMemoryInBytes { + seelog.Warnf("Task %s container %s memory setting is too low, increasing to %d bytes", task.Arn, container.Name, DockerContainerMinimumMemoryInBytes) + dockerMem = DockerContainerMinimumMemoryInBytes + } + cpuShare := task.dockerCPUShares(container.CPU) + + // Docker copied Memory and cpu field into hostconfig in 1.6 with api version(1.18) + // https://github.com/moby/moby/commit/837eec064d2d40a4d86acbc6f47fada8263e0d4c + dockerAPIVersion, err := docker.NewAPIVersion(string(apiVersion)) + if err != nil { + seelog.Errorf("Creating docker api version failed, err: %v", err) + return err + } + + dockerAPIVersion_1_18 := docker.APIVersion([]int{1, 18}) + if dockerAPIVersion.GreaterThanOrEqualTo(dockerAPIVersion_1_18) { + // Set the memory and cpu in host config + if hc != nil { + hc.Memory = dockerMem + hc.CPUShares = cpuShare + } + return nil + } + + // Set the memory and cpu in config + if config != nil { + config.Memory = dockerMem + config.CPUShares = cpuShare + } + + return nil +} + // dockerCPUShares converts containerCPU shares if needed as per the logic stated below: // Docker silently converts 0 to 1024 CPU shares, which is probably not what we // want. Instead, we convert 0 to 2 to be closer to expected behavior. The @@ -512,8 +553,8 @@ func (task *Task) dockerConfigVolumes(container *Container) (map[string]struct{} } // DockerHostConfig construct the configuration recognized by docker -func (task *Task) DockerHostConfig(container *Container, dockerContainerMap map[string]*DockerContainer) (*docker.HostConfig, *HostConfigError) { - return task.dockerHostConfig(container, dockerContainerMap) +func (task *Task) DockerHostConfig(container *Container, dockerContainerMap map[string]*DockerContainer, apiVersion dockerclient.DockerVersion) (*docker.HostConfig, *HostConfigError) { + return task.dockerHostConfig(container, dockerContainerMap, apiVersion) } // ApplyExecutionRoleLogsAuth will check whether the task has excecution role @@ -538,7 +579,7 @@ func (task *Task) ApplyExecutionRoleLogsAuth(hostConfig *docker.HostConfig, cred return nil } -func (task *Task) dockerHostConfig(container *Container, dockerContainerMap map[string]*DockerContainer) (*docker.HostConfig, *HostConfigError) { +func (task *Task) dockerHostConfig(container *Container, dockerContainerMap map[string]*DockerContainer, apiVersion dockerclient.DockerVersion) (*docker.HostConfig, *HostConfigError) { dockerLinkArr, err := task.dockerLinks(container, dockerContainerMap) if err != nil { return nil, &HostConfigError{err.Error()} @@ -564,6 +605,11 @@ func (task *Task) dockerHostConfig(container *Container, dockerContainerMap map[ VolumesFrom: volumesFrom, } + err = task.SetConfigHostconfigBasedOnVersion(container, nil, hostConfig, apiVersion) + if err != nil { + return nil, &HostConfigError{err.Error()} + } + if container.DockerConfig.HostConfig != nil { err := json.Unmarshal([]byte(*container.DockerConfig.HostConfig), hostConfig) if err != nil { diff --git a/agent/api/task_test.go b/agent/api/task_test.go index 4f26b3feef2..dd2048f33d2 100644 --- a/agent/api/task_test.go +++ b/agent/api/task_test.go @@ -16,6 +16,7 @@ package api import ( "encoding/json" "reflect" + "runtime" "testing" "time" @@ -23,6 +24,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" + "github.com/aws/amazon-ecs-agent/agent/engine/dockerclient" "github.com/aws/amazon-ecs-agent/agent/utils/ttime" docker "github.com/fsouza/go-dockerclient" "github.com/golang/mock/gomock" @@ -31,6 +33,8 @@ import ( const dockerIDPrefix = "dockerid-" +var defaultDockerClientAPIVersion = dockerclient.Version_1_17 + func strptr(s string) *string { return &s } func dockerMap(task *Task) map[string]*DockerContainer { @@ -51,7 +55,7 @@ func TestDockerConfigPortBinding(t *testing.T) { }, } - config, err := testTask.DockerConfig(testTask.Containers[0]) + config, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err != nil { t.Error(err) } @@ -76,7 +80,7 @@ func TestDockerConfigCPUShareZero(t *testing.T) { }, } - config, err := testTask.DockerConfig(testTask.Containers[0]) + config, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err != nil { t.Error(err) } @@ -96,7 +100,7 @@ func TestDockerConfigCPUShareMinimum(t *testing.T) { }, } - config, err := testTask.DockerConfig(testTask.Containers[0]) + config, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err != nil { t.Error(err) } @@ -116,7 +120,7 @@ func TestDockerConfigCPUShareUnchanged(t *testing.T) { }, } - config, err := testTask.DockerConfig(testTask.Containers[0]) + config, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err != nil { t.Error(err) } @@ -136,7 +140,7 @@ func TestDockerHostConfigPortBinding(t *testing.T) { }, } - config, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + config, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) bindings, ok := config.PortBindings["10/tcp"] @@ -163,7 +167,7 @@ func TestDockerHostConfigVolumesFrom(t *testing.T) { }, } - config, err := testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask)) + config, err := testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) if !reflect.DeepEqual(config.VolumesFrom, []string{"dockername-c1"}) { @@ -179,6 +183,7 @@ func TestDockerHostConfigRawConfig(t *testing.T) { DNSSearch: []string{"dns.search"}, ExtraHosts: []string{"extra:hosts"}, SecurityOpt: []string{"foo", "bar"}, + CPUShares: 2, LogConfig: docker.LogConfig{ Type: "foo", Config: map[string]string{"foo": "bar"}, @@ -206,10 +211,15 @@ func TestDockerHostConfigRawConfig(t *testing.T) { }, } - config, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + config, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, configErr) expectedOutput := rawHostConfigInput + expectedOutput.CPUPercent = minimumCPUPercent + if runtime.GOOS == "windows" { + // CPUShares will always be 0 on windows + expectedOutput.CPUShares = 0 + } assertSetStructFieldsEqual(t, expectedOutput, *config) } @@ -250,7 +260,7 @@ func TestDockerHostConfigRawConfigMerging(t *testing.T) { }, } - hostConfig, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + hostConfig, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, configErr) expected := docker.HostConfig{ @@ -258,6 +268,7 @@ func TestDockerHostConfigRawConfigMerging(t *testing.T) { SecurityOpt: []string{"foo", "bar"}, VolumesFrom: []string{"dockername-c2"}, MemorySwappiness: memorySwappinessDefault, + CPUPercent: minimumCPUPercent, } assertSetStructFieldsEqual(t, expected, *hostConfig) @@ -285,18 +296,18 @@ func TestDockerHostConfigPauseContainer(t *testing.T) { // Verify that the network mode is set to "container:" // for a non empty volume, non pause container - config, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + config, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) assert.Equal(t, "container:"+dockerIDPrefix+PauseContainerName, config.NetworkMode) // Verify that the network mode is not set to "none" for the // empty volume container - config, err = testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask)) + config, err = testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) assert.Equal(t, networkModeNone, config.NetworkMode) // Verify that the network mode is set to "none" for the pause container - config, err = testTask.DockerHostConfig(testTask.Containers[2], dockerMap(testTask)) + config, err = testTask.DockerHostConfig(testTask.Containers[2], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) assert.Equal(t, networkModeNone, config.NetworkMode) @@ -307,13 +318,13 @@ func TestDockerHostConfigPauseContainer(t *testing.T) { // DNS overrides are only applied to the pause container. Verify that the non-pause // container contains no overrides - config, err = testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + config, err = testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) assert.Equal(t, 0, len(config.DNS)) assert.Equal(t, 0, len(config.DNSSearch)) // Verify DNS settings are overridden for the pause container - config, err = testTask.DockerHostConfig(testTask.Containers[2], dockerMap(testTask)) + config, err = testTask.DockerHostConfig(testTask.Containers[2], dockerMap(testTask), defaultDockerClientAPIVersion) assert.Nil(t, err) assert.Equal(t, []string{"169.254.169.253"}, config.DNS) assert.Equal(t, []string{"us-west-2.compute.internal"}, config.DNSSearch) @@ -334,7 +345,7 @@ func TestBadDockerHostConfigRawConfig(t *testing.T) { }, }, } - _, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(&testTask)) + _, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(&testTask), defaultDockerClientAPIVersion) assert.Error(t, err) } } @@ -368,7 +379,7 @@ func TestDockerConfigRawConfig(t *testing.T) { }, } - config, configErr := testTask.DockerConfig(testTask.Containers[0]) + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if configErr != nil { t.Fatal(configErr) } @@ -399,7 +410,7 @@ func TestDockerConfigRawConfigNilLabel(t *testing.T) { }, } - _, configErr := testTask.DockerConfig(testTask.Containers[0]) + _, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if configErr != nil { t.Fatal(configErr) } @@ -428,7 +439,7 @@ func TestDockerConfigRawConfigMerging(t *testing.T) { Name: "c1", Image: "image", CPU: 50, - Memory: 100, + Memory: 1000, DockerConfig: DockerConfig{ Config: strptr(string(rawConfig)), }, @@ -436,13 +447,13 @@ func TestDockerConfigRawConfigMerging(t *testing.T) { }, } - config, configErr := testTask.DockerConfig(testTask.Containers[0]) + config, configErr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if configErr != nil { t.Fatal(configErr) } expected := docker.Config{ - Memory: 100 * 1024 * 1024, + Memory: 1000 * 1024 * 1024, CPUShares: 50, Image: "image", User: "user", @@ -466,7 +477,7 @@ func TestBadDockerConfigRawConfig(t *testing.T) { }, }, } - _, err := testTask.DockerConfig(testTask.Containers[0]) + _, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err == nil { t.Fatal("Expected error, was none for: " + badConfig) } @@ -1172,7 +1183,7 @@ func TestApplyExecutionRoleLogsAuthSet(t *testing.T) { credentialsManager.EXPECT().GetTaskCredentials(credentialsIDInTask).Return(taskCredentials, true) task.initializeCredentialsEndpoint(credentialsManager) - config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task)) + config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task), defaultDockerClientAPIVersion) assert.Nil(t, err) err = task.ApplyExecutionRoleLogsAuth(config, credentialsManager) @@ -1216,7 +1227,7 @@ func TestApplyExecutionRoleLogsAuthFailEmptyCredentialsID(t *testing.T) { task.initializeCredentialsEndpoint(credentialsManager) - config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task)) + config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task), defaultDockerClientAPIVersion) assert.Nil(t, err) err = task.ApplyExecutionRoleLogsAuth(config, credentialsManager) @@ -1260,9 +1271,84 @@ func TestApplyExecutionRoleLogsAuthFailNoCredentialsForTask(t *testing.T) { credentialsManager.EXPECT().GetTaskCredentials(credentialsIDInTask).Return(credentials.TaskIAMRoleCredentials{}, false) task.initializeCredentialsEndpoint(credentialsManager) - config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task)) + config, err := task.DockerHostConfig(task.Containers[0], dockerMap(task), defaultDockerClientAPIVersion) assert.Error(t, err) err = task.ApplyExecutionRoleLogsAuth(config, credentialsManager) assert.Error(t, err) } + +// TestSetConfigHostconfigBasedOnAPIVersion tests the docker hostconfig was correctly set// based on the docker client version +func TestSetConfigHostconfigBasedOnAPIVersion(t *testing.T) { + memoryMiB := 500 + testTask := &Task{ + Containers: []*Container{ + { + Name: "c1", + CPU: uint(10), + Memory: uint(memoryMiB), + }, + }, + } + + hostconfig, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) + assert.Nil(t, err) + + config, cerr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + assert.Nil(t, cerr) + + assert.Equal(t, int64(memoryMiB*1024*1024), config.Memory) + if runtime.GOOS == "windows" { + assert.Equal(t, int64(minimumCPUPercent), hostconfig.CPUPercent) + } else { + assert.Equal(t, int64(10), config.CPUShares) + } + assert.Empty(t, hostconfig.CPUShares) + assert.Empty(t, hostconfig.Memory) + + hostconfig, err = testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), dockerclient.Version_1_18) + assert.Nil(t, err) + + config, cerr = testTask.DockerConfig(testTask.Containers[0], dockerclient.Version_1_18) + assert.Nil(t, err) + assert.Equal(t, int64(memoryMiB*1024*1024), hostconfig.Memory) + if runtime.GOOS == "windows" { + // cpushares is set to zero on windows + assert.Empty(t, hostconfig.CPUShares) + assert.Equal(t, int64(minimumCPUPercent), hostconfig.CPUPercent) + } else { + assert.Equal(t, int64(10), hostconfig.CPUShares) + } + + assert.Empty(t, config.CPUShares) + assert.Empty(t, config.Memory) +} + +// TestSetMinimumMemoryLimit ensures that we set the correct minimum memory limit when the limit is too low +func TestSetMinimumMemoryLimit(t *testing.T) { + testTask := &Task{ + Containers: []*Container{ + { + Name: "c1", + Memory: uint(1), + }, + }, + } + + hostconfig, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) + assert.Nil(t, err) + + config, cerr := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + assert.Nil(t, cerr) + + assert.Equal(t, int64(DockerContainerMinimumMemoryInBytes), config.Memory) + assert.Empty(t, hostconfig.Memory) + + hostconfig, err = testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), dockerclient.Version_1_18) + assert.Nil(t, err) + + config, cerr = testTask.DockerConfig(testTask.Containers[0], dockerclient.Version_1_18) + assert.Nil(t, err) + assert.Equal(t, int64(DockerContainerMinimumMemoryInBytes), hostconfig.Memory) + assert.Empty(t, config.Memory) +} diff --git a/agent/api/task_unix.go b/agent/api/task_unix.go index a247fd8b136..ba65391d554 100644 --- a/agent/api/task_unix.go +++ b/agent/api/task_unix.go @@ -36,7 +36,8 @@ const ( // Reference: http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html minimumCPUShare = 2 - bytesPerMegabyte = 1024 * 1024 + minimumCPUPercent = 0 + bytesPerMegabyte = 1024 * 1024 ) func (task *Task) adjustForPlatform(cfg *config.Config) { diff --git a/agent/api/task_unix_test.go b/agent/api/task_unix_test.go index f77c1da4482..7e0785c4953 100644 --- a/agent/api/task_unix_test.go +++ b/agent/api/task_unix_test.go @@ -293,7 +293,7 @@ func TestPlatformHostConfigOverrideErrorPath(t *testing.T) { }, } - dockerHostConfig, err := task.DockerHostConfig(task.Containers[0], dockerMap(task)) + dockerHostConfig, err := task.DockerHostConfig(task.Containers[0], dockerMap(task), defaultDockerClientAPIVersion) assert.Error(t, err) assert.Empty(t, dockerHostConfig) } diff --git a/agent/api/task_windows.go b/agent/api/task_windows.go index 97d1c47b6ff..c1966d0dcc4 100644 --- a/agent/api/task_windows.go +++ b/agent/api/task_windows.go @@ -17,6 +17,7 @@ package api import ( "path/filepath" + "runtime" "strings" "github.com/aws/amazon-ecs-agent/agent/config" @@ -26,8 +27,14 @@ import ( const ( //memorySwappinessDefault is the expected default value for this platform memorySwappinessDefault = -1 + // cpuSharesPerCore represents the cpu shares of a cpu core in docker + cpuSharesPerCore = 1024 + percentageFactor = 100 + minimumCPUPercent = 1 ) +var cpuShareScaleFactor = runtime.NumCPU() * cpuSharesPerCore + // adjustForPlatform makes Windows-specific changes to the task after unmarshal func (task *Task) adjustForPlatform(cfg *config.Config) { task.downcaseAllVolumePaths() @@ -59,6 +66,13 @@ func getCanonicalPath(path string) string { // passed to Docker API. func (task *Task) platformHostConfigOverride(hostConfig *docker.HostConfig) error { task.overrideDefaultMemorySwappiness(hostConfig) + // Convert the CPUShares to CPUPercent + hostConfig.CPUPercent = hostConfig.CPUShares * percentageFactor / int64(cpuShareScaleFactor) + if hostConfig.CPUPercent == 0 { + // if the cpu percent is too low, we set it to the minimum + hostConfig.CPUPercent = minimumCPUPercent + } + hostConfig.CPUShares = 0 return nil } diff --git a/agent/api/task_windows_test.go b/agent/api/task_windows_test.go index b72a908ad10..7009606f2fa 100644 --- a/agent/api/task_windows_test.go +++ b/agent/api/task_windows_test.go @@ -112,11 +112,17 @@ func TestWindowsPlatformHostConfigOverride(t *testing.T) { task := &Task{} - hostConfig := &docker.HostConfig{} + hostConfig := &docker.HostConfig{CPUShares: int64(1 * cpuSharesPerCore)} task.platformHostConfigOverride(hostConfig) - + assert.Equal(t, int64(1*cpuSharesPerCore*percentageFactor)/int64(cpuShareScaleFactor), hostConfig.CPUPercent) + assert.Equal(t, int64(0), hostConfig.CPUShares) assert.EqualValues(t, expectedMemorySwappinessDefault, hostConfig.MemorySwappiness) + + hostConfig = &docker.HostConfig{CPUShares: 10} + task.platformHostConfigOverride(hostConfig) + assert.Equal(t, int64(minimumCPUPercent), hostConfig.CPUPercent) + assert.Empty(t, hostConfig.CPUShares) } func TestWindowsMemorySwappinessOption(t *testing.T) { @@ -142,7 +148,7 @@ func TestWindowsMemorySwappinessOption(t *testing.T) { }, } - config, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask)) + config, configErr := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion) if configErr != nil { t.Fatal(configErr) } diff --git a/agent/app/agent.go b/agent/app/agent.go index c3490398e50..948ff843283 100644 --- a/agent/app/agent.go +++ b/agent/app/agent.go @@ -46,9 +46,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/amazon-ecs-agent/agent/version" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" aws_credentials "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/cihub/seelog" ) @@ -76,8 +76,12 @@ type agent interface { // printECSAttributes prints the Agent's capabilities based on // its environment printECSAttributes() int + // startWindowsService starts the agent as a Windows Service + startWindowsService() int // start starts the Agent execution start() int + // setTerminationHandler sets the termination handler + setTerminationHandler(sighandlers.TerminationHandler) } // ecsAgent wraps all the entities needed to start the ECS Agent execution. @@ -100,9 +104,10 @@ type ecsAgent struct { mac string metadataManager containermetadata.Manager resource resources.Resource + terminationHandler sighandlers.TerminationHandler } -// newAgent returns a new ecsAgent object +// newAgent returns a new ecsAgent object, but does not start anything func newAgent( ctx context.Context, blackholeEC2Metadata bool, @@ -158,9 +163,10 @@ func newAgent( PluginsPath: cfg.CNIPluginsPath, MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion, }), - os: oswrapper.New(), - metadataManager: metadataManager, - resource: resources.New(), + os: oswrapper.New(), + metadataManager: metadataManager, + resource: resources.New(), + terminationHandler: sighandlers.StartDefaultTerminationHandler, }, nil } @@ -184,6 +190,10 @@ func (agent *ecsAgent) printECSAttributes() int { return exitcodes.ExitSuccess } +func (agent *ecsAgent) setTerminationHandler(handler sighandlers.TerminationHandler) { + agent.terminationHandler = handler +} + // start starts the ECS Agent func (agent *ecsAgent) start() int { sighandlers.StartDebugHandler() @@ -514,7 +524,7 @@ func (agent *ecsAgent) startAsyncRoutines( go imageManager.StartImageCleanupProcess(agent.ctx) } - go sighandlers.StartTerminationHandler(stateManager, taskEngine) + go agent.terminationHandler(stateManager, taskEngine) // Agent introspection api go handlers.ServeHttp(&agent.containerInstanceARN, taskEngine, agent.cfg) diff --git a/agent/app/agent_unix.go b/agent/app/agent_unix.go index 5575363e33d..b8ff77fa815 100644 --- a/agent/app/agent_unix.go +++ b/agent/app/agent_unix.go @@ -41,6 +41,12 @@ var awsVPCCNIPlugins = []string{ecscni.ECSENIPluginName, ecscni.ECSIPAMPluginName, } +// startWindowsService is not supported on Linux +func (agent *ecsAgent) startWindowsService() int { + seelog.Error("Windows Services are not supported on Linux") + return 1 +} + // initializeTaskENIDependencies initializes all of the dependencies required by // the Agent to support the 'awsvpc' networking mode. A non nil error is returned // if an error is encountered during this process. An additional boolean flag to diff --git a/agent/app/agent_unix_test.go b/agent/app/agent_unix_test.go index 7e43ef6859c..605a681bc7a 100644 --- a/agent/app/agent_unix_test.go +++ b/agent/app/agent_unix_test.go @@ -39,6 +39,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/aws/amazon-ecs-agent/agent/resources/mock_resources" "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" + "github.com/aws/amazon-ecs-agent/agent/statemanager" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/golang/mock/gomock" @@ -100,6 +101,7 @@ func TestDoStartHappyPath(t *testing.T) { cfg: &cfg, credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -198,6 +200,7 @@ func TestDoStartTaskENIHappyPath(t *testing.T) { cniClient: cniClient, os: mockOS, ec2MetadataClient: mockMetadata, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -497,6 +500,7 @@ func TestDoStartCgroupInitHappyPath(t *testing.T) { credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, resource: mockResource, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -537,6 +541,7 @@ func TestDoStartCgroupInitErrorPath(t *testing.T) { credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, resource: mockResource, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } status := agent.doStart(eventstream.NewEventStream("events", ctx), diff --git a/agent/app/agent_windows.go b/agent/app/agent_windows.go new file mode 100644 index 00000000000..7f8e5e31417 --- /dev/null +++ b/agent/app/agent_windows.go @@ -0,0 +1,242 @@ +// +build windows + +// Copyright 2017 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 app + +import ( + "context" + "sync" + "time" + + "github.com/aws/amazon-ecs-agent/agent/engine" + "github.com/aws/amazon-ecs-agent/agent/sighandlers" + "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" + "github.com/aws/amazon-ecs-agent/agent/statemanager" + "github.com/cihub/seelog" + "golang.org/x/sys/windows/svc" +) + +const ( + //EcsSvcName is the name of the service + EcsSvcName = "AmazonECS" +) + +// startWindowsService runs the ECS agent as a Windows Service +func (agent *ecsAgent) startWindowsService() int { + svc.Run(EcsSvcName, newHandler(agent)) + return 0 +} + +// handler implements https://godoc.org/golang.org/x/sys/windows/svc#Handler +type handler struct { + ecsAgent agent +} + +func newHandler(agent agent) *handler { + return &handler{agent} +} + +// Execute implements https://godoc.org/golang.org/x/sys/windows/svc#Handler +// The basic way that this implementation works is through two channels (representing the requests from Windows and the +// responses we're sending to Windows) and two goroutines (one for message processing with Windows and the other to +// actually run the agent). Once we've set everything up and started both goroutines, we wait for either one to exit +// (the Windows goroutine will exit based on messages from Windows while the agent goroutine exits if the agent exits) +// and then cancel the other. Once everything has stopped running, this function returns and the process exits. +func (h *handler) Execute(args []string, requests <-chan svc.ChangeRequest, responses chan<- svc.Status) (bool, uint32) { + defer seelog.Flush() + // channels for communication between goroutines + ctx, cancel := context.WithCancel(context.Background()) + agentDone := make(chan struct{}) + windowsDone := make(chan struct{}) + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + defer close(windowsDone) + defer wg.Done() + h.handleWindowsRequests(ctx, requests, responses) + }() + + var agentExitCode uint32 + go func() { + defer close(agentDone) + defer wg.Done() + agentExitCode = h.runAgent(ctx) + }() + + // Wait until one of the goroutines is either told to stop or fails spectacularly. Under normal conditions we will + // be waiting here for a long time. + select { + case <-windowsDone: + // Service was told to stop by the Windows API. This happens either through manual intervention (i.e., + // "Stop-Service AmazonECS") or through system shutdown. Regardless, this is a normal exit event and not an + // error. + seelog.Info("Received normal signal from Windows to exit") + case <-agentDone: + // This means that the agent stopped on its own. This is where it's appropriate to light the event log on fire + // and set off all the alarms. + seelog.Errorf("Exiting %d", agentExitCode) + } + cancel() + wg.Wait() + seelog.Infof("Bye Bye! Exiting with %d", agentExitCode) + return true, agentExitCode +} + +// handleWindowsRequests is a loop intended to run in a goroutine. It handles bidirectional communication with the +// Windows service manager. This function works by pretty much immediately moving to running and then waiting for a +// stop or shut down message from Windows or to be canceled (which could happen if the agent exits by itself and the +// calling function cancels the context). +func (h *handler) handleWindowsRequests(ctx context.Context, requests <-chan svc.ChangeRequest, responses chan<- svc.Status) { + // Immediately tell Windows that we are pending service start. + responses <- svc.Status{State: svc.StartPending} + seelog.Info("Starting Windows service") + + // TODO: Pre-start hooks go here (unclear if we need any yet) + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682108(v=vs.85).aspx + // Not sure if a better link exists to describe what these values mean + accepts := svc.AcceptStop | svc.AcceptShutdown + + // Announce that we are running and we accept the above-mentioned commands + responses <- svc.Status{State: svc.Running, Accepts: accepts} + + defer func() { + // Announce that we are stopping + seelog.Info("Stopping Windows service") + responses <- svc.Status{State: svc.StopPending} + }() + + for { + select { + case <-ctx.Done(): + return + case r := <-requests: + switch r.Cmd { + case svc.Interrogate: + // Our status doesn't change unless we are told to stop or shutdown + responses <- r.CurrentStatus + case svc.Stop, svc.Shutdown: + return + default: + continue + } + } + } +} + +// runAgent runs the ECS agent inside a goroutine and waits to be told to exit. +func (h *handler) runAgent(ctx context.Context) uint32 { + agentCtx, cancel := context.WithCancel(ctx) + indicator := newTermHandlerIndicator() + + terminationHandler := func(saver statemanager.Saver, taskEngine engine.TaskEngine) { + // We're using a custom indicator to record that the handler is scheduled to be executed (has been invoked) and + // to determine whether it should run (we skip when the agent engine has already exited). After recording to + // the indicator that the handler has been invoked, we wait on the context. When we wake up, we determine + // whether to execute or not based on whether the agent is still running. + defer indicator.finish() + indicator.setInvoked() + <-agentCtx.Done() + if !indicator.isAgentRunning() { + return + } + + seelog.Info("Termination handler received signal to stop") + err := sighandlers.FinalSave(saver, taskEngine) + if err != nil { + seelog.Criticalf("Error saving state before final shutdown: %v", err) + } + } + h.ecsAgent.setTerminationHandler(terminationHandler) + + go func() { + defer cancel() + exitCode := h.ecsAgent.start() // should block forever, unless there is an error + + if exitCode == exitcodes.ExitTerminal { + seelog.Critical("Terminal exit code received from agent. Windows SCM will not restart the AmazonECS service.") + // We override the exit code to 0 here so that Windows does not treat this as a restartable failure even + // when "sc.exe failureflag" is set. + exitCode = 0 + } + + indicator.agentStopped(exitCode) + }() + + sleepCtx(agentCtx, time.Minute) // give the agent a minute to start and invoke terminationHandler + + // wait for the termination handler to run. Once the termination handler runs, we can safely exit. If the agent + // exits by itself, the termination handler doesn't need to do anything and skips. If the agent exits before the + // termination handler is invoked, we can exit immediately. + return indicator.wait() +} + +// sleepCtx provides a cancelable sleep +func sleepCtx(ctx context.Context, duration time.Duration) { + derivedCtx, _ := context.WithDeadline(ctx, time.Now().Add(duration)) + <-derivedCtx.Done() +} + +type termHandlerIndicator struct { + mu sync.Mutex + agentRunning bool + exitCode uint32 + handlerInvoked bool + handlerDone chan struct{} +} + +func newTermHandlerIndicator() *termHandlerIndicator { + return &termHandlerIndicator{ + agentRunning: true, + handlerInvoked: false, + handlerDone: make(chan struct{}), + } +} + +func (t *termHandlerIndicator) isAgentRunning() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.agentRunning +} + +func (t *termHandlerIndicator) agentStopped(exitCode int) { + t.mu.Lock() + defer t.mu.Unlock() + t.agentRunning = false + t.exitCode = uint32(exitCode) +} + +func (t *termHandlerIndicator) finish() { + close(t.handlerDone) +} + +func (t *termHandlerIndicator) setInvoked() { + t.mu.Lock() + defer t.mu.Unlock() + t.handlerInvoked = true +} + +func (t *termHandlerIndicator) wait() uint32 { + t.mu.Lock() + invoked := t.handlerInvoked + t.mu.Unlock() + if invoked { + <-t.handlerDone + } + t.mu.Lock() + defer t.mu.Unlock() + return t.exitCode +} diff --git a/agent/app/agent_windows_test.go b/agent/app/agent_windows_test.go index 9dbe4ebe975..5be445caa5a 100644 --- a/agent/app/agent_windows_test.go +++ b/agent/app/agent_windows_test.go @@ -19,65 +19,251 @@ import ( "context" "sync" "testing" + "time" - app_mocks "github.com/aws/amazon-ecs-agent/agent/app/mocks" "github.com/aws/amazon-ecs-agent/agent/engine" - "github.com/aws/amazon-ecs-agent/agent/eventstream" - "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/amazon-ecs-agent/agent/sighandlers" + statemanager_mocks "github.com/aws/amazon-ecs-agent/agent/statemanager/mocks" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/sys/windows/svc" ) -// TestDoStartHappyPath tests the doStart method for windows. This method should -// go away when we support metrics for windows containers -func TestDoStartHappyPath(t *testing.T) { - ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _ := setup(t) +type mockAgent struct { + startFunc func() int + terminationHandler sighandlers.TerminationHandler +} + +func (m *mockAgent) start() int { + return m.startFunc() +} +func (m *mockAgent) setTerminationHandler(handler sighandlers.TerminationHandler) { + m.terminationHandler = handler +} +func (m *mockAgent) printVersion() int { return 0 } +func (m *mockAgent) printECSAttributes() int { return 0 } +func (m *mockAgent) startWindowsService() int { return 0 } + +func TestHandler_RunAgent_StartExitImmediately(t *testing.T) { + // register some mocks, but nothing should get called on any of them + ctrl := gomock.NewController(t) + _ = statemanager_mocks.NewMockStateManager(ctrl) + _ = engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + wg := sync.WaitGroup{} + wg.Add(1) + startFunc := func() int { + // startFunc doesn't block, nothing is called + wg.Done() + return 0 + } + agent := &mockAgent{startFunc: startFunc} + handler := &handler{agent} + go handler.runAgent(context.TODO()) + wg.Wait() + assert.NotNil(t, agent.terminationHandler) +} + +func TestHandler_RunAgent_NoSaveWithNoTerminationHandler(t *testing.T) { + // register some mocks, but nothing should get called on any of them + ctrl := gomock.NewController(t) + _ = statemanager_mocks.NewMockStateManager(ctrl) + _ = engine.NewMockTaskEngine(ctrl) defer ctrl.Finish() - mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - - var discoverEndpointsInvoked sync.WaitGroup - discoverEndpointsInvoked.Add(1) - containerChangeEvents := make(chan engine.DockerContainerChangeEvent) - - // These calls are expected to happen, but cannot be ordered as they are - // invoked via go routines, which will lead to occasional test failues - dockerClient.EXPECT().Version().AnyTimes() - imageManager.EXPECT().StartImageCleanupProcess(gomock.Any()).MaxTimes(1) - mockCredentialsProvider.EXPECT().IsExpired().Return(false).AnyTimes() - client.EXPECT().DiscoverPollEndpoint(gomock.Any()).Do(func(x interface{}) { - // Ensures that the test waits until acs session has bee started - discoverEndpointsInvoked.Done() - }).Return("poll-endpoint", nil) - client.EXPECT().DiscoverPollEndpoint(gomock.Any()).Return("acs-endpoint", nil).AnyTimes() - - gomock.InOrder( - mockCredentialsProvider.EXPECT().Retrieve().Return(credentials.Value{}, nil), - dockerClient.EXPECT().SupportedVersions().Return(nil), - dockerClient.EXPECT().KnownVersions().Return(nil), - client.EXPECT().RegisterContainerInstance(gomock.Any(), gomock.Any()).Return("arn", nil), - imageManager.EXPECT().SetSaver(gomock.Any()), - dockerClient.EXPECT().ContainerEvents(gomock.Any()).Return(containerChangeEvents, nil), - state.EXPECT().AllImageStates().Return(nil), - state.EXPECT().AllTasks().Return(nil), - ) - - cfg := getTestConfig() + done := make(chan struct{}) + startFunc := func() int { + <-done // block until after the test ends so that we can test that runAgent returns when cancelled + return 0 + } + agent := &mockAgent{startFunc: startFunc} + handler := &handler{agent} ctx, cancel := context.WithCancel(context.TODO()) - // Cancel the context to cancel async routines - defer cancel() - agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - credentialProvider: credentials.NewCredentials(mockCredentialsProvider), - dockerClient: dockerClient, + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + handler.runAgent(ctx) + wg.Done() + }() + cancel() + wg.Wait() + assert.NotNil(t, agent.terminationHandler) +} + +func TestHandler_RunAgent_ForceSaveWithTerminationHandler(t *testing.T) { + ctrl := gomock.NewController(t) + stateManager := statemanager_mocks.NewMockStateManager(ctrl) + taskEngine := engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + taskEngine.EXPECT().Disable() + stateManager.EXPECT().ForceSave() + + agent := &mockAgent{} + + done := make(chan struct{}) + defer func() { done <- struct{}{} }() + startFunc := func() int { + go agent.terminationHandler(stateManager, taskEngine) + <-done // block until after the test ends so that we can test that runAgent returns when cancelled + return 0 } + agent.startFunc = startFunc + handler := &handler{agent} + ctx, cancel := context.WithCancel(context.TODO()) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + handler.runAgent(ctx) + wg.Done() + }() + time.Sleep(time.Second) // give startFunc enough time to actually call the termination handler + cancel() + wg.Wait() +} + +func TestHandler_HandleWindowsRequests_StopService(t *testing.T) { + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + handler := &handler{} + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + handler.handleWindowsRequests(context.TODO(), requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}} + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate") + requests <- svc.ChangeRequest{Cmd: svc.Stop} + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Stop") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_HandleWindowsRequests_Cancel(t *testing.T) { + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + handler := &handler{} + ctx, cancel := context.WithCancel(context.TODO()) + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + handler.handleWindowsRequests(ctx, requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}} + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate") + cancel() + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Cancel") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_Execute_WindowsStops(t *testing.T) { + ctrl := gomock.NewController(t) + stateManager := statemanager_mocks.NewMockStateManager(ctrl) + taskEngine := engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + taskEngine.EXPECT().Disable() + stateManager.EXPECT().ForceSave() + + agent := &mockAgent{} + + done := make(chan struct{}) + defer func() { done <- struct{}{} }() + startFunc := func() int { + go agent.terminationHandler(stateManager, taskEngine) + <-done // block until after the test ends so that we can test that Execute returns when Stopped + return 0 + } + agent.startFunc = startFunc + handler := &handler{agent} + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + handler.Execute(nil, requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + time.Sleep(time.Second) // let it run for a second + requests <- svc.ChangeRequest{Cmd: svc.Shutdown} + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Shutdown") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_Execute_AgentStops(t *testing.T) { + agent := &mockAgent{} + + ctx, cancel := context.WithCancel(context.TODO()) + startFunc := func() int { + <-ctx.Done() + return 0 + } + agent.startFunc = startFunc + handler := &handler{agent} + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + handler.Execute(nil, requests, responses) + wg.Done() + }() - go agent.doStart(eventstream.NewEventStream("events", ctx), - credentialsManager, state, imageManager, client) + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + time.Sleep(time.Second) // let it run for a second + cancel() + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after agent goroutine stops") + wg.Done() + }() - // Wait for both DiscoverPollEndpointInput and DiscoverTelemetryEndpoint to be - // invoked. These are used as proxies to indicate that acs and tcs handlers' - // NewSession call has been invoked - discoverEndpointsInvoked.Wait() + wg.Wait() } diff --git a/agent/app/args/flag.go b/agent/app/args/flag.go index 56b92569b98..a1a078e78af 100644 --- a/agent/app/args/flag.go +++ b/agent/app/args/flag.go @@ -18,10 +18,11 @@ import "flag" const ( versionUsage = "Print the agent version information and exit" logLevelUsage = "Loglevel: [||||]" + ecsAttributesUsage = "Print the Agent's ECS Attributes based on its environment" acceptInsecureCertUsage = "Disable SSL certificate verification. We do not recommend setting this option" licenseUsage = "Print the LICENSE and NOTICE files and exit" blacholeEC2MetadataUsage = "Blackhole the EC2 Metadata requests. Setting this option can cause the ECS Agent to fail to work properly. We do not recommend setting this option" - ecsAttributesUsage = "Print the Agent's ECS Attributes based on its environment" + windowsServiceUsage = "Run the ECS agent as a Windows Service" versionFlagName = "version" logLevelFlagName = "loglevel" @@ -29,6 +30,7 @@ const ( acceptInsecureCertFlagName = "k" licenseFlagName = "license" blackholeEC2MetadataFlagName = "blackhole-ec2-metadata" + windowsServiceFlagName = "windows-service" ) // Args wraps various ECS Agent arguments @@ -47,6 +49,8 @@ type Args struct { BlackholeEC2Metadata *bool // ECSAttributes indicates that the agent should print its attributes ECSAttributes *bool + // WindowsService indicates that the agent should run as a Windows service + WindowsService *bool } // New creates a new Args object from the argument list @@ -60,6 +64,7 @@ func New(arguments []string) (*Args, error) { License: flagset.Bool(licenseFlagName, false, licenseUsage), BlackholeEC2Metadata: flagset.Bool(blackholeEC2MetadataFlagName, false, blacholeEC2MetadataUsage), ECSAttributes: flagset.Bool(ecsAttributesFlagName, false, ecsAttributesUsage), + WindowsService: flagset.Bool(windowsServiceFlagName, false, windowsServiceUsage), } err := flagset.Parse(arguments) diff --git a/agent/app/run.go b/agent/app/run.go index 487ed630f6e..944d7054540 100644 --- a/agent/app/run.go +++ b/agent/app/run.go @@ -56,6 +56,9 @@ func Run(arguments []string) int { case *parsedArgs.ECSAttributes: // Print agent's ecs attributes based on its environment and exit return agent.printECSAttributes() + case *parsedArgs.WindowsService: + // Enable Windows Service + return agent.startWindowsService() default: // Start the agent return agent.start() diff --git a/agent/config/config_unix.go b/agent/config/config_unix.go index fa35d366f4b..27139f9870b 100644 --- a/agent/config/config_unix.go +++ b/agent/config/config_unix.go @@ -21,6 +21,8 @@ import ( ) const ( + // AgentCredentialsAddress is used to serve the credentials for tasks. + AgentCredentialsAddress = "" // this is left blank right now for net=bridge // defaultAuditLogFile specifies the default audit log filename defaultCredentialsAuditLogFile = "/log/audit.log" // Default cgroup prefix for ECS tasks diff --git a/agent/config/config_windows.go b/agent/config/config_windows.go index 9f1dd81d688..03c37bf6faf 100644 --- a/agent/config/config_windows.go +++ b/agent/config/config_windows.go @@ -23,6 +23,9 @@ import ( ) const ( + // AgentCredentialsAddress is used to serve the credentials for tasks. + AgentCredentialsAddress = "127.0.0.1" + // defaultAuditLogFile specifies the default audit log filename defaultCredentialsAuditLogFile = `log\audit.log` // When using IAM roles for tasks on Windows, the credential proxy consumes port 80 httpPort = 80 @@ -63,9 +66,7 @@ func DefaultConfig() Config { DataDir: dataDir, // DataDirOnHost is identical to DataDir for Windows because we do not // run as a container - DataDirOnHost: dataDir, - // DisableMetrics is set to true on Windows as docker stats does not work - DisableMetrics: true, + DataDirOnHost: dataDir, ReservedMemory: 0, AvailableLoggingDrivers: []dockerclient.LoggingDriver{dockerclient.JSONFileDriver, dockerclient.NoneDriver}, TaskCleanupWaitDuration: DefaultTaskCleanupWaitDuration, diff --git a/agent/config/config_windows_test.go b/agent/config/config_windows_test.go index 812f9917d0e..776a9859be9 100644 --- a/agent/config/config_windows_test.go +++ b/agent/config/config_windows_test.go @@ -34,7 +34,7 @@ func TestConfigDefault(t *testing.T) { assert.Equal(t, "npipe:////./pipe/docker_engine", cfg.DockerEndpoint, "Default docker endpoint set incorrectly") assert.Equal(t, `C:\ProgramData\Amazon\ECS\data`, cfg.DataDir, "Default datadir set incorrectly") - assert.True(t, cfg.DisableMetrics, "Default disablemetrics set incorrectly") + assert.False(t, cfg.DisableMetrics, "Default disablemetrics set incorrectly") assert.Equal(t, 10, len(cfg.ReservedPorts), "Default reserved ports set incorrectly") assert.Equal(t, uint16(0), cfg.ReservedMemory, "Default reserved memory set incorrectly") assert.Equal(t, 30*time.Second, cfg.DockerStopTimeout, "Default docker stop container timeout set incorrectly") diff --git a/agent/engine/docker_container_engine.go b/agent/engine/docker_container_engine.go index ae884c5dfea..c8c1311a4aa 100644 --- a/agent/engine/docker_container_engine.go +++ b/agent/engine/docker_container_engine.go @@ -146,6 +146,8 @@ type DockerClient interface { // Version returns the version of the Docker daemon. Version() (string, error) + // APIVersion returns the api version of the client + APIVersion() (dockerclient.DockerVersion, error) // InspectImage returns information about the specified image. InspectImage(string) (*docker.Image, error) @@ -884,6 +886,15 @@ func (dg *dockerGoClient) Version() (string, error) { return info.Get("Version"), nil } +// APIVersion returns the client api version +func (dg *dockerGoClient) APIVersion() (dockerclient.DockerVersion, error) { + client, err := dg.dockerClient() + if err != nil { + return "", err + } + return dg.clientFactory.FindClientAPIVersion(client), nil +} + // Stats returns a channel of *docker.Stats entries for the container. func (dg *dockerGoClient) Stats(id string, ctx context.Context) (<-chan *docker.Stats, error) { client, err := dg.dockerClient() diff --git a/agent/engine/docker_image_manager_integ_test.go b/agent/engine/docker_image_manager_integ_test.go index f17627faadd..54bd2f4fdac 100644 --- a/agent/engine/docker_image_manager_integ_test.go +++ b/agent/engine/docker_image_manager_integ_test.go @@ -352,7 +352,7 @@ func TestImageWithSameNameAndDifferentID(t *testing.T) { err = renameImage(test3Image3Name, "testimagewithsamenameanddifferentid", "latest", goDockerClient) require.NoError(t, err, "Renaming the image failed") - // Start and wiat for task3 to be running + // Start and wait for task3 to be running go taskEngine.AddTask(task3) err = verifyTaskIsRunning(stateChangeEvents, task3) require.NoError(t, err, "task3") @@ -566,24 +566,24 @@ func createImageCleanupHappyTestTask(taskName string) *api.Task { Image: test1Image1Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, { Name: "test2", Image: test1Image2Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, { Name: "test3", Image: test1Image3Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, }, } @@ -601,24 +601,24 @@ func createImageCleanupThresholdTestTask(taskName string) *api.Task { Image: test2Image1Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, { Name: "test2", Image: test2Image2Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, { Name: "test3", Image: test2Image3Name, Essential: false, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 10, - Memory: 10, + CPU: 512, + Memory: 256, }, }, } diff --git a/agent/engine/docker_task_engine.go b/agent/engine/docker_task_engine.go index 54a735a5363..25866f40db9 100644 --- a/agent/engine/docker_task_engine.go +++ b/agent/engine/docker_task_engine.go @@ -685,7 +685,12 @@ func (engine *DockerTaskEngine) createContainer(task *api.Task, container *api.C // we have to do this in create, not start, because docker no longer handles // merging create config with start hostconfig the same; e.g. memory limits // get lost - hostConfig, hcerr := task.DockerHostConfig(container, containerMap) + dockerClientVersion, versionErr := client.APIVersion() + if versionErr != nil { + return DockerContainerMetadata{Error: CannotGetDockerClientVersionError{versionErr}} + } + + hostConfig, hcerr := task.DockerHostConfig(container, containerMap, dockerClientVersion) if hcerr != nil { return DockerContainerMetadata{Error: api.NamedError(hcerr)} } @@ -697,7 +702,7 @@ func (engine *DockerTaskEngine) createContainer(task *api.Task, container *api.C } } - config, err := task.DockerConfig(container) + config, err := task.DockerConfig(container, dockerClientVersion) if err != nil { return DockerContainerMetadata{Error: api.NamedError(err)} } diff --git a/agent/engine/docker_task_engine_test.go b/agent/engine/docker_task_engine_test.go index 426fd5b20c1..e8362f57d7a 100644 --- a/agent/engine/docker_task_engine_test.go +++ b/agent/engine/docker_task_engine_test.go @@ -30,6 +30,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks" + "github.com/aws/amazon-ecs-agent/agent/engine/dockerclient" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/image" "github.com/aws/amazon-ecs-agent/agent/engine/testdata" @@ -55,6 +56,7 @@ const ( ) var defaultConfig = config.DefaultConfig() +var defaultDockerClientAPIVersion = dockerclient.Version_1_17 func mocks(t *testing.T, cfg *config.Config) (*gomock.Controller, *MockDockerClient, *mock_ttime.MockTime, TaskEngine, *mock_credentials.MockManager, *MockImageManager, *mock_containermetadata.MockManager) { ctrl := gomock.NewController(t) @@ -106,7 +108,7 @@ func TestBatchContainerHappyPath(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container).Return(nil) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -119,6 +121,7 @@ func TestBatchContainerHappyPath(t *testing.T) { dockerConfig.Labels["com.amazonaws.ecs.task-definition-family"] = sleepTask.Family dockerConfig.Labels["com.amazonaws.ecs.task-definition-version"] = sleepTask.Version dockerConfig.Labels["com.amazonaws.ecs.cluster"] = "" + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, y interface{}, containerName string, z time.Duration) { @@ -249,6 +252,7 @@ func TestBatchContainerHappyPath(t *testing.T) { // TestContainerMetadataEnabledHappyPath checks case when metadata service is enabled and does not have errors func TestContainerMetadataEnabledHappyPath(t *testing.T) { metadataConfig := defaultConfig + metadataConfig.TaskCPUMemLimit = config.ExplicitlyDisabled metadataConfig.ContainerMetadataEnabled = true ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager := mocks(t, &metadataConfig) defer ctrl.Finish() @@ -275,7 +279,7 @@ func TestContainerMetadataEnabledHappyPath(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container).Return(nil) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -289,6 +293,7 @@ func TestContainerMetadataEnabledHappyPath(t *testing.T) { dockerConfig.Labels["com.amazonaws.ecs.task-definition-version"] = sleepTask.Version dockerConfig.Labels["com.amazonaws.ecs.cluster"] = "" metadataManager.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, y interface{}, containerName string, z time.Duration) { @@ -445,7 +450,8 @@ func TestContainerMetadataEnabledErrorPath(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container).Return(nil) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -637,6 +643,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { gomock.InOrder( // Ensure that the pause container is created first + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, hostConfig *docker.HostConfig, containerName string, z time.Duration) { sleepTask.SetTaskENI(&api.ENI{ @@ -679,6 +686,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { mockCNIClient.EXPECT().SetupNS(gomock.Any()).Return(nil), // Once the pause container is started, sleep container will be created + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, hostConfig *docker.HostConfig, containerName string, z time.Duration) { assert.True(t, strings.Contains(containerName, sleepContainer.Name)) @@ -787,6 +795,7 @@ func TestRemoveEvents(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container).Return(nil) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, y interface{}, containerName string, z time.Duration) { createdContainerName = containerName @@ -917,13 +926,14 @@ func TestStartTimeoutThenStart(t *testing.T) { client.EXPECT().Version() client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) for _, container := range sleepTask.Containers { imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes() client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -1001,7 +1011,8 @@ func TestSteadyStatePoll(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) assert.Nil(t, err) // Container config should get updated with this during CreateContainer @@ -1178,6 +1189,7 @@ func TestCreateContainerForceSave(t *testing.T) { sleepTask := testdata.LoadTask("sleep5") sleepContainer, _ := sleepTask.ContainerByName("sleep5") + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).AnyTimes() gomock.InOrder( saver.EXPECT().ForceSave().Do(func() interface{} { task, ok := taskEngine.state.TaskByArn(sleepTask.Arn) @@ -1213,7 +1225,7 @@ func TestCreateContainerMergesLabels(t *testing.T) { }, }, } - expectedConfig, err := testTask.DockerConfig(testTask.Containers[0]) + expectedConfig, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -1225,6 +1237,7 @@ func TestCreateContainerMergesLabels(t *testing.T) { "com.amazonaws.ecs.cluster": "", "key": "value", } + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).AnyTimes() client.EXPECT().CreateContainer(expectedConfig, gomock.Any(), gomock.Any(), gomock.Any()) taskEngine.(*DockerTaskEngine).createContainer(testTask, testTask.Containers[0]) } @@ -1256,7 +1269,8 @@ func TestTaskTransitionWhenStopContainerTimesout(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(container) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) - dockerConfig, err := sleepTask.DockerConfig(container) + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) + dockerConfig, err := sleepTask.DockerConfig(container, defaultDockerClientAPIVersion) if err != nil { t.Fatal(err) } @@ -1367,6 +1381,7 @@ func TestTaskTransitionWhenStopContainerReturnsUnretriableError(t *testing.T) { client.EXPECT().PullImage(container.Image, nil).Return(DockerContainerMetadata{}), imageManager.EXPECT().RecordContainerReference(container), imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil), + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), // Simulate successful create container client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(x, y, z, timeout interface{}) { @@ -1464,6 +1479,7 @@ func TestTaskTransitionWhenStopContainerReturnsTransientErrorBeforeSucceeding(t imageManager.EXPECT().RecordContainerReference(container), imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil), // Simulate successful create container + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return( DockerContainerMetadata{DockerID: containerID}), // Simulate successful start container @@ -1615,6 +1631,7 @@ func TestPauseContaienrHappyPath(t *testing.T) { pauseContainerID := "pauseContainerID" // Pause container will be launched first gomock.InOrder( + dockerClient.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), dockerClient.EXPECT().CreateContainer( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(config *docker.Config, x, y, z interface{}) { @@ -1637,6 +1654,7 @@ func TestPauseContaienrHappyPath(t *testing.T) { dockerClient.EXPECT().PullImage(gomock.Any(), nil).Return(DockerContainerMetadata{}) imageManager.EXPECT().RecordContainerReference(gomock.Any()).Return(nil) imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil) + dockerClient.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) dockerClient.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(DockerContainerMetadata{DockerID: containerID}) dockerClient.EXPECT().StartContainer(containerID, startContainerTimeout).Return( @@ -1860,6 +1878,7 @@ func TestCreateContainerOnAgentRestart(t *testing.T) { state.AddContainer(&api.DockerContainer{DockerName: "docker_container_name", Container: sleepContainer}, sleepTask) gomock.InOrder( + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), "docker_container_name", gomock.Any()), ) diff --git a/agent/engine/dockerclient/dockerclientfactory.go b/agent/engine/dockerclient/dockerclientfactory.go index 9b869b346b3..10bc3ca9aba 100644 --- a/agent/engine/dockerclient/dockerclientfactory.go +++ b/agent/engine/dockerclient/dockerclientfactory.go @@ -27,10 +27,11 @@ const ( // https://docs.docker.com/engine/api/version-history/#v125-api-changes minAPIVersionKey = "MinAPIVersion" // apiVersionKey is the docker.Env key for API version - apiVersionKey = "ApiVersion" + apiVersionKey = "ApiVersion" // zeroPatch is a string to append patch number zero if the major minor version lacks it zeroPatch = ".0" ) + // Factory provides a collection of docker remote clients that include a // recommended client version as well as a set of alternative supported // docker clients. @@ -57,6 +58,9 @@ type Factory interface { // not necessarily fully support) and the versions that result in // successful responses by the Docker daemon. FindKnownAPIVersions() []DockerVersion + + // FindClientAPIVersion returns the client api version + FindClientAPIVersion(dockeriface.Client) DockerVersion } type factory struct { @@ -106,6 +110,18 @@ func (f *factory) FindKnownAPIVersions() []DockerVersion { return knownVersions } +// FindClientAPIVersion returns the version of the client from the map +// TODO we should let go docker client return this version information +func (f *factory) FindClientAPIVersion(client dockeriface.Client) DockerVersion { + for k, v := range f.clients { + if v == client { + return k + } + } + + return getDefaultVersion() +} + // getClient returns a client specified by the docker version. Its wrapped // by GetClient so that it can do platform-specific magic func (f *factory) getClient(version DockerVersion) (dockeriface.Client, error) { @@ -157,7 +173,7 @@ func getDockerClientForVersion( version string, minAPIVersion string, apiVersion string) (dockeriface.Client, error) { - if (minAPIVersion != "" && apiVersion != "") { + if minAPIVersion != "" && apiVersion != "" { // Adding patch number zero to Docker versions to reuse the existing semver // comparator // TODO: remove this logic later when non-semver comparator is implemented diff --git a/agent/engine/dockerclient/dockerclientfactory_unix_test.go b/agent/engine/dockerclient/dockerclientfactory_unix_test.go index 9492495418e..62986d808b6 100644 --- a/agent/engine/dockerclient/dockerclientfactory_unix_test.go +++ b/agent/engine/dockerclient/dockerclientfactory_unix_test.go @@ -19,9 +19,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dockeriface" "github.com/aws/amazon-ecs-agent/agent/engine/dockeriface/mocks" + docker "github.com/fsouza/go-dockerclient" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - docker "github.com/fsouza/go-dockerclient" ) func TestGetClientCached(t *testing.T) { @@ -44,3 +44,13 @@ func TestGetClientCached(t *testing.T) { assert.Equal(t, client, clientAgain) } + +func TestFindClientAPIVersion(t *testing.T) { + factory := NewFactory(expectedEndpoint) + + for _, version := range getAgentVersions() { + client, err := factory.GetClient(version) + assert.NoError(t, err) + assert.Equal(t, version, factory.FindClientAPIVersion(client)) + } +} diff --git a/agent/engine/dockerclient/dockerclientfactory_windows_test.go b/agent/engine/dockerclient/dockerclientfactory_windows_test.go index a2c007d3f6c..fef3cce654d 100644 --- a/agent/engine/dockerclient/dockerclientfactory_windows_test.go +++ b/agent/engine/dockerclient/dockerclientfactory_windows_test.go @@ -19,9 +19,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dockeriface" "github.com/aws/amazon-ecs-agent/agent/engine/dockeriface/mocks" + docker "github.com/fsouza/go-dockerclient" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - docker "github.com/fsouza/go-dockerclient" ) func TestGetClientMinimumVersion(t *testing.T) { @@ -46,3 +46,13 @@ func TestGetClientMinimumVersion(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedClient, actualClient) } + +func TestFindClientAPIVersion(t *testing.T) { + factory := NewFactory(expectedEndpoint) + + for _, version := range getAgentVersions() { + client, err := factory.GetClient(version) + assert.NoError(t, err) + assert.Equal(t, Version_1_24, factory.FindClientAPIVersion(client)) + } +} diff --git a/agent/engine/dockerclient/mocks/dockerclient_mocks.go b/agent/engine/dockerclient/mocks/dockerclient_mocks.go index 475e1b99b4d..2b585f43f03 100644 --- a/agent/engine/dockerclient/mocks/dockerclient_mocks.go +++ b/agent/engine/dockerclient/mocks/dockerclient_mocks.go @@ -43,6 +43,16 @@ func (_m *MockFactory) EXPECT() *_MockFactoryRecorder { return _m.recorder } +func (_m *MockFactory) FindClientAPIVersion(_param0 dockeriface.Client) dockerclient.DockerVersion { + ret := _m.ctrl.Call(_m, "FindClientAPIVersion", _param0) + ret0, _ := ret[0].(dockerclient.DockerVersion) + return ret0 +} + +func (_mr *_MockFactoryRecorder) FindClientAPIVersion(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "FindClientAPIVersion", arg0) +} + func (_m *MockFactory) FindKnownAPIVersions() []dockerclient.DockerVersion { ret := _m.ctrl.Call(_m, "FindKnownAPIVersions") ret0, _ := ret[0].([]dockerclient.DockerVersion) diff --git a/agent/engine/engine_mocks.go b/agent/engine/engine_mocks.go index 3541ddd8cf7..d1fe95f02d6 100644 --- a/agent/engine/engine_mocks.go +++ b/agent/engine/engine_mocks.go @@ -181,6 +181,17 @@ func (_m *MockDockerClient) EXPECT() *_MockDockerClientRecorder { return _m.recorder } +func (_m *MockDockerClient) APIVersion() (dockerclient.DockerVersion, error) { + ret := _m.ctrl.Call(_m, "APIVersion") + ret0, _ := ret[0].(dockerclient.DockerVersion) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockDockerClientRecorder) APIVersion() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "APIVersion") +} + func (_m *MockDockerClient) ContainerEvents(_param0 context0.Context) (<-chan DockerContainerChangeEvent, error) { ret := _m.ctrl.Call(_m, "ContainerEvents", _param0) ret0, _ := ret[0].(<-chan DockerContainerChangeEvent) diff --git a/agent/engine/engine_unix_integ_test.go b/agent/engine/engine_unix_integ_test.go index b09da5259db..50007ff0019 100644 --- a/agent/engine/engine_unix_integ_test.go +++ b/agent/engine/engine_unix_integ_test.go @@ -542,9 +542,9 @@ func TestDockerAuth(t *testing.T) { event = <-stateChangeEvents assert.Equal(t, event.(api.TaskStateChange).Status, api.TaskRunning, "Expected task to be RUNNING") - taskUpdate := *testTask + taskUpdate := createTestTask("testDockerAuth") taskUpdate.SetDesiredStatus(api.TaskStopped) - go taskEngine.AddTask(&taskUpdate) + go taskEngine.AddTask(taskUpdate) event = <-stateChangeEvents assert.Equal(t, event.(api.ContainerStateChange).Status, api.ContainerStopped, "Expected container to be STOPPED") diff --git a/agent/engine/engine_windows_integ_test.go b/agent/engine/engine_windows_integ_test.go index 809b42bd673..b7f9ef45e79 100644 --- a/agent/engine/engine_windows_integ_test.go +++ b/agent/engine/engine_windows_integ_test.go @@ -31,8 +31,8 @@ func createTestContainer() *api.Container { Image: "microsoft/windowsservercore:latest", Essential: true, DesiredStatusUnsafe: api.ContainerRunning, - CPU: 100, - Memory: 80, + CPU: 512, + Memory: 256, } } diff --git a/agent/engine/errors.go b/agent/engine/errors.go index a9638d924ad..43375e9fa04 100644 --- a/agent/engine/errors.go +++ b/agent/engine/errors.go @@ -317,3 +317,16 @@ func (err ContainerNetworkingError) Error() string { func (err ContainerNetworkingError) ErrorName() string { return "ContainerNetworkingError" } + +// CannotGetDockerClientVersionError indicates error when trying to get docker +// client api version +type CannotGetDockerClientVersionError struct { + fromError error +} + +func (err CannotGetDockerClientVersionError) ErrorName() string { + return "CannotGetDockerClientVersionError" +} +func (err CannotGetDockerClientVersionError) Error() string { + return err.fromError.Error() +} diff --git a/agent/functional_tests/testdata/taskdefinitions/awslogs-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/awslogs-windows/task-definition.json index ae44b7f67dc..9bc608d9df1 100644 --- a/agent/functional_tests/testdata/taskdefinitions/awslogs-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/awslogs-windows/task-definition.json @@ -2,9 +2,9 @@ "family": "ecs-awslogs-test", "containerDefinitions": [{ "essential": true, - "memory": 10, + "memory": 256, "name": "awslogs", - "cpu": 10, + "cpu": 512, "image": "microsoft/windowsservercore:latest", "logConfiguration": { "logDriver": "awslogs", diff --git a/agent/functional_tests/testdata/taskdefinitions/cleanup-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/cleanup-windows/task-definition.json index 734091a79dd..48cdf0207ad 100644 --- a/agent/functional_tests/testdata/taskdefinitions/cleanup-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/cleanup-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "cleanup-windows", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "portBindings": [{ "containerPort": 80 }], diff --git a/agent/functional_tests/testdata/taskdefinitions/datavolume-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/datavolume-windows/task-definition.json index d02d311b947..85e56bac546 100644 --- a/agent/functional_tests/testdata/taskdefinitions/datavolume-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/datavolume-windows/task-definition.json @@ -7,8 +7,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "exit", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "essential": true, "volumesFrom": [{ "sourceContainer": "data-volume-source" @@ -17,8 +17,8 @@ }, { "image": "microsoft/windowsservercore:latest", "name": "dataSource", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "essential": false, "volumesFrom": [{ "sourceContainer": "data-volume-source" @@ -27,8 +27,8 @@ }, { "image": "microsoft/windowsservercore:latest", "name": "data-volume-source", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "essential": false, "mountPoints": [{ "sourceVolume": "test", diff --git a/agent/functional_tests/testdata/taskdefinitions/hostname-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/hostname-windows/task-definition.json index a44488a02c3..6dac1a877cc 100644 --- a/agent/functional_tests/testdata/taskdefinitions/hostname-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/hostname-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "exit", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "hostname": "foobarbaz", "command": ["powershell", "-c", "if ((hostname) -eq \"foobarbaz\") { exit 42 } ; exit 1"] }] diff --git a/agent/functional_tests/testdata/taskdefinitions/iam-roles-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/iam-roles-windows/task-definition.json index 2fdd26968da..68a4565171c 100644 --- a/agent/functional_tests/testdata/taskdefinitions/iam-roles-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/iam-roles-windows/task-definition.json @@ -2,8 +2,8 @@ "family": "ecsftest-iamrole-test", "taskRoleArn": "$$$TASK_ROLE$$$", "containerDefinitions": [{ - "memory": 100, - "cpu": 100, + "memory": 512, + "cpu": 1024, "name": "container-with-iamrole-windows", "image": "amazon/amazon-ecs-iamrolecontainer", "entryPoint": ["powershell"], diff --git a/agent/functional_tests/testdata/taskdefinitions/labels-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/labels-windows/task-definition.json index 583881a3db7..80324c25661 100644 --- a/agent/functional_tests/testdata/taskdefinitions/labels-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/labels-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "labeled", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "dockerLabels": { "label1": "", "com.foo.label2": "value" diff --git a/agent/functional_tests/testdata/taskdefinitions/logdriver-jsonfile-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/logdriver-jsonfile-windows/task-definition.json index 0b95d2699f8..f57349bf2c7 100644 --- a/agent/functional_tests/testdata/taskdefinitions/logdriver-jsonfile-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/logdriver-jsonfile-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "exit", - "memory": 10, - "cpu": 10, + "memory": 256, + "cpu": 512, "logConfiguration": { "logDriver": "json-file", "options": { diff --git a/agent/functional_tests/testdata/taskdefinitions/mdservice-validator-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/mdservice-validator-windows/task-definition.json index 6d81554166c..584ed2add98 100644 --- a/agent/functional_tests/testdata/taskdefinitions/mdservice-validator-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/mdservice-validator-windows/task-definition.json @@ -4,8 +4,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "mdservice-validator-windows", - "cpu": 100, - "memory": 100, + "cpu": 1024, + "memory": 512, "entryPoint": ["powershell"], "command": ["-c", "sleep 10; if($?){if(cat $env:ECS_CONTAINER_METADATA_FILE | Select-String -pattern READY){exit 42}else {exit 1}};"] }] diff --git a/agent/functional_tests/testdata/taskdefinitions/network-mode-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/network-mode-windows/task-definition.json index def33ae7859..3a25d32f72f 100644 --- a/agent/functional_tests/testdata/taskdefinitions/network-mode-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/network-mode-windows/task-definition.json @@ -6,6 +6,7 @@ "entryPoint": ["powershell"], "command": ["sleep", "60"], "name": "network-$$$$NETWORK_MODE$$$$", - "memory": 50 + "memory": 256, + "cpu": 512 }] } diff --git a/agent/functional_tests/testdata/taskdefinitions/oom-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/oom-windows/task-definition.json new file mode 100644 index 00000000000..caa1bd420c1 --- /dev/null +++ b/agent/functional_tests/testdata/taskdefinitions/oom-windows/task-definition.json @@ -0,0 +1,12 @@ +{ + "family": "ecsftest-oom-container-windows", + "containerDefinitions": [{ + "essential": true, + "memory": 256, + "name": "memory-overcommit", + "cpu": 512, + "image": "amazon/amazon-ecs-windows-python:make", + "command": ["python", "-c", "import time; time.sleep(30); foo=' '*1024*1024*1024;"] + }] +} + diff --git a/agent/functional_tests/testdata/taskdefinitions/port-80-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/port-80-windows/task-definition.json index 3ee74298516..d9fe3d19342 100644 --- a/agent/functional_tests/testdata/taskdefinitions/port-80-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/port-80-windows/task-definition.json @@ -7,7 +7,8 @@ "containerPort": 80, "hostPort": 5180 }], - "memory": 50, + "memory": 256, + "cpu": 512, "command": ["powershell", "\\listen80.exe"] }] } diff --git a/agent/functional_tests/testdata/taskdefinitions/savedstate-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/savedstate-windows/task-definition.json index 0a3e4e5ad43..4d7ccabc9b4 100644 --- a/agent/functional_tests/testdata/taskdefinitions/savedstate-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/savedstate-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "savedstate-windows", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "entryPoint": ["powershell"], "command": ["sleep", "500"] }] diff --git a/agent/functional_tests/testdata/taskdefinitions/simple-exit-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/simple-exit-windows/task-definition.json index 683d5cd4494..86a06b301ea 100644 --- a/agent/functional_tests/testdata/taskdefinitions/simple-exit-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/simple-exit-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "exit", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "essential": true, "entryPoint": ["powershell"], "command": ["exit", "42"] diff --git a/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json new file mode 100644 index 00000000000..2cf925b9233 --- /dev/null +++ b/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json @@ -0,0 +1,9 @@ +{ + "family": "ecsftest-windows-telemetry", + "containerDefinitions": [{ + "image": "amazon/amazon-ecs-windows-cpupercent-test:make", + "name": "windows-cpu-percent", + "cpu": $$$$CPUSHARE$$$$, + "memory": 256 + }] +} diff --git a/agent/functional_tests/testdata/taskdefinitions/working-dir-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/working-dir-windows/task-definition.json index 1e1325da152..2bf7079c4ef 100644 --- a/agent/functional_tests/testdata/taskdefinitions/working-dir-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/working-dir-windows/task-definition.json @@ -3,8 +3,8 @@ "containerDefinitions": [{ "image": "microsoft/windowsservercore:latest", "name": "exit", - "cpu": 10, - "memory": 10, + "cpu": 512, + "memory": 256, "workingDirectory": "C:/windows/system32", "command": ["powershell", "-c", "if ((pwd).Path -eq \"C:\\windows\\system32\") { exit 42 } ; exit 1"] }] diff --git a/agent/functional_tests/tests/functionaltests_unix_test.go b/agent/functional_tests/tests/functionaltests_unix_test.go index 302a517f462..d43acf9879c 100644 --- a/agent/functional_tests/tests/functionaltests_unix_test.go +++ b/agent/functional_tests/tests/functionaltests_unix_test.go @@ -383,11 +383,11 @@ func TestTelemetry(t *testing.T) { time.Sleep(waitMetricsInCloudwatchDuration) cwclient := cloudwatch.New(session.New(), aws.NewConfig().WithRegion(*ECS.Config.Region)) - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for memory utilization failed") testTask, err := agent.StartTask(t, "telemetry") @@ -400,11 +400,11 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, false) + _, err = VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, false) + _, err = VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for memory utilization failed") err = testTask.Stop() @@ -417,11 +417,11 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped: verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped, verify metrics for memory utilization failed") } diff --git a/agent/functional_tests/tests/functionaltests_windows_test.go b/agent/functional_tests/tests/functionaltests_windows_test.go index 29401eedcb7..5331c63ef75 100644 --- a/agent/functional_tests/tests/functionaltests_windows_test.go +++ b/agent/functional_tests/tests/functionaltests_windows_test.go @@ -18,15 +18,20 @@ package functional_tests import ( "fmt" "os" + "runtime" + "strconv" "strings" "testing" "time" + ecsapi "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" . "github.com/aws/amazon-ecs-agent/agent/functional_tests/util" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/pborman/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,6 +43,7 @@ const ( logDriverTaskDefinition = "logdriver-jsonfile-windows" cleanupTaskDefinition = "cleanup-windows" networkModeTaskDefinition = "network-mode-windows" + cpuSharesPerCore = 1024 ) // TestAWSLogsDriver verifies that container logs are sent to Amazon CloudWatch Logs with awslogs as the log driver @@ -171,7 +177,7 @@ func taskIamRolesTest(networkMode string, agent *TestAgent, t *testing.T) { } // Task will only run one command "aws ec2 describe-regions" - err = task.WaitStopped(2 * time.Minute) + err = task.WaitStopped(waitTaskStateChangeDuration) if err != nil { t.Fatalf("Waiting task to stop error : %v", err) } @@ -209,7 +215,7 @@ func TestMetadataServiceValidator(t *testing.T) { } // clean up - err = task.WaitStopped(2 * time.Minute) + err = task.WaitStopped(waitTaskStateChangeDuration) require.NoError(t, err, "Error waiting for task to transition to STOPPED") containerID, err := agent.ResolveTaskDockerID(task, "mdservice-validator-windows") @@ -223,3 +229,106 @@ func TestMetadataServiceValidator(t *testing.T) { exitCode := containerMetaData.State.ExitCode assert.Equal(t, 42, exitCode, fmt.Sprintf("Expected exit code of 42; got %d", exitCode)) } + +// TestTelemetry tests whether agent can send metrics to TACS +func TestTelemetry(t *testing.T) { + // Try to use a new cluster for this test, ensure no other task metrics for this cluster + newClusterName := "ecstest-telemetry-" + uuid.New() + _, err := ECS.CreateCluster(&ecsapi.CreateClusterInput{ + ClusterName: aws.String(newClusterName), + }) + require.NoError(t, err, "Failed to create cluster") + defer DeleteCluster(t, newClusterName) + + agentOptions := AgentOptions{ + ExtraEnvironment: map[string]string{ + "ECS_CLUSTER": newClusterName, + }, + } + agent := RunAgent(t, &agentOptions) + defer agent.Cleanup() + + params := &cloudwatch.GetMetricStatisticsInput{ + MetricName: aws.String("CPUUtilization"), + Namespace: aws.String("AWS/ECS"), + Period: aws.Int64(60), + Statistics: []*string{ + aws.String("Average"), + aws.String("SampleCount"), + }, + Dimensions: []*cloudwatch.Dimension{ + { + Name: aws.String("ClusterName"), + Value: aws.String(newClusterName), + }, + }, + } + params.StartTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) + params.EndTime = aws.Time((*params.StartTime).Add(waitMetricsInCloudwatchDuration).UTC()) + // wait for the agent start and ensure no task is running + time.Sleep(waitMetricsInCloudwatchDuration) + + cwclient := cloudwatch.New(session.New(), aws.NewConfig().WithRegion(*ECS.Config.Region)) + _, err = VerifyMetrics(cwclient, params, true) + assert.NoError(t, err, "Before task running, verify metrics for CPU utilization failed") + + params.MetricName = aws.String("MemoryUtilization") + _, err = VerifyMetrics(cwclient, params, true) + assert.NoError(t, err, "Before task running, verify metrics for memory utilization failed") + + cpuNum := runtime.NumCPU() + + tdOverrides := make(map[string]string) + // Set the container cpu percentage 25% + tdOverrides["$$$$CPUSHARE$$$$"] = strconv.Itoa(int(float64(cpuNum*cpuSharesPerCore) * 0.25)) + + testTask, err := agent.StartTaskWithTaskDefinitionOverrides(t, "telemetry-windows", tdOverrides) + require.NoError(t, err, "Failed to start telemetry task") + // Wait for the task to run and the agent to send back metrics + err = testTask.WaitRunning(waitTaskStateChangeDuration) + require.NoError(t, err, "Error wait telemetry task running") + + time.Sleep(waitMetricsInCloudwatchDuration) + params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) + params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) + params.MetricName = aws.String("CPUUtilization") + metrics, err := VerifyMetrics(cwclient, params, false) + assert.NoError(t, err, "Task is running, verify metrics for CPU utilization failed") + // Also verify the cpu usage is around 25% + assert.InDelta(t, 0.25, *metrics.Average, 0.05) + + params.MetricName = aws.String("MemoryUtilization") + _, err = VerifyMetrics(cwclient, params, false) + assert.NoError(t, err, "Task is running, verify metrics for memory utilization failed") + + err = testTask.Stop() + require.NoError(t, err, "Failed to stop the telemetry task") + + err = testTask.WaitStopped(waitTaskStateChangeDuration) + require.NoError(t, err, "Waiting for task stop failed") + + time.Sleep(waitMetricsInCloudwatchDuration) + params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) + params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) + params.MetricName = aws.String("CPUUtilization") + _, err = VerifyMetrics(cwclient, params, true) + assert.NoError(t, err, "Task stopped: verify metrics for CPU utilization failed") + + params.MetricName = aws.String("MemoryUtilization") + _, err = VerifyMetrics(cwclient, params, true) + assert.NoError(t, err, "Task stopped, verify metrics for memory utilization failed") +} + +// TestOOMContainer verifies that an OOM container returns an error +func TestOOMContainer(t *testing.T) { + agent := RunAgent(t, nil) + defer agent.Cleanup() + + testTask, err := agent.StartTask(t, "oom-windows") + require.NoError(t, err, "Expected to start invalid-image task") + err = testTask.WaitRunning(waitTaskStateChangeDuration) + assert.NoError(t, err, "Expect task to be running") + err = testTask.WaitStopped(waitTaskStateChangeDuration) + assert.NoError(t, err, "Expect task to be stopped") + assert.NotEqual(t, 0, testTask.Containers[0].ExitCode, "container should fail with memory error") +} diff --git a/agent/functional_tests/util/utils.go b/agent/functional_tests/util/utils.go index 90867537da8..8eb1e0f50ab 100644 --- a/agent/functional_tests/util/utils.go +++ b/agent/functional_tests/util/utils.go @@ -266,36 +266,36 @@ func DeleteCluster(t *testing.T, clusterName string) { // VerifyMetrics whether the response is as expected // the expected value can be 0 or positive -func VerifyMetrics(cwclient *cloudwatch.CloudWatch, params *cloudwatch.GetMetricStatisticsInput, idleCluster bool) error { +func VerifyMetrics(cwclient *cloudwatch.CloudWatch, params *cloudwatch.GetMetricStatisticsInput, idleCluster bool) (*cloudwatch.Datapoint, error) { resp, err := cwclient.GetMetricStatistics(params) if err != nil { - return fmt.Errorf("Error getting metrics of cluster: %v", err) + return nil, fmt.Errorf("Error getting metrics of cluster: %v", err) } if resp == nil || resp.Datapoints == nil { - return fmt.Errorf("Cloudwatch get metrics failed, returned null") + return nil, fmt.Errorf("Cloudwatch get metrics failed, returned null") } metricsCount := len(resp.Datapoints) if metricsCount == 0 { - return fmt.Errorf("No datapoints returned") + return nil, fmt.Errorf("No datapoints returned") } datapoint := resp.Datapoints[metricsCount-1] // Samplecount is always expected to be "1" for cluster metrics if *datapoint.SampleCount != 1.0 { - return fmt.Errorf("Incorrect SampleCount %f, expected 1", *datapoint.SampleCount) + return nil, fmt.Errorf("Incorrect SampleCount %f, expected 1", *datapoint.SampleCount) } if idleCluster { if *datapoint.Average != 0.0 { - return fmt.Errorf("non-zero utilization for idle cluster") + return nil, fmt.Errorf("non-zero utilization for idle cluster") } } else { if *datapoint.Average == 0.0 { - return fmt.Errorf("utilization is zero for non-idle cluster") + return nil, fmt.Errorf("utilization is zero for non-idle cluster") } } - return nil + return datapoint, nil } // ResolveTaskDockerID determines the Docker ID for a container within a given diff --git a/agent/functional_tests/util/utils_windows.go b/agent/functional_tests/util/utils_windows.go index 0bffb7ca99e..d0b656b1284 100644 --- a/agent/functional_tests/util/utils_windows.go +++ b/agent/functional_tests/util/utils_windows.go @@ -88,7 +88,7 @@ func RunAgent(t *testing.T, options *AgentOptions) *TestAgent { os.Setenv("ECS_CLUSTER", Cluster) os.Setenv("ECS_ENABLE_TASK_IAM_ROLE", "true") os.Setenv("DOCKER_HOST", "npipe:////./pipe/docker_engine") - os.Setenv("ECS_DISABLE_METRICS", "true") + os.Setenv("ECS_DISABLE_METRICS", "false") os.Setenv("ECS_AUDIT_LOGFILE", logdir+"/audit.log") os.Setenv("ECS_LOGLEVEL", "debug") os.Setenv("ECS_AVAILABLE_LOGGING_DRIVERS", `["json-file","awslogs"]`) diff --git a/agent/handlers/credentials/handler.go b/agent/handlers/credentials/handler.go index dc5d27b1867..26a5b08f020 100644 --- a/agent/handlers/credentials/handler.go +++ b/agent/handlers/credentials/handler.go @@ -106,7 +106,7 @@ func setupServer(credentialsManager credentials.Manager, auditLogger audit.Audit loggingServeMux.Handle("/", handlers.NewLoggingHandler(serverMux)) server := http.Server{ - Addr: ":" + strconv.Itoa(config.AgentCredentialsPort), + Addr: config.AgentCredentialsAddress + ":" + strconv.Itoa(config.AgentCredentialsPort), Handler: loggingServeMux, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, diff --git a/agent/logger/eventlog_windows.go b/agent/logger/eventlog_windows.go new file mode 100644 index 00000000000..a2057a768a2 --- /dev/null +++ b/agent/logger/eventlog_windows.go @@ -0,0 +1,84 @@ +// +build windows + +// Copyright 2017 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 logger + +import ( + "github.com/cihub/seelog" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/eventlog" +) + +/* +TODO: Make this whole thing better + +What's here right now is a stub just so that agent logs can appear in the Event Log. Longer term, we should do a few +things to make this better: + +1) Don't use init() and a package-global variable +2) Conform to the MSDN guidelines about event log data. + +References to MSDN guidelines can be found here: +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363632(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363648(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363661(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363669(v=vs.85).aspx +*/ + +const ( + eventLogName = "AmazonECSAgent" + eventLogID = 999 +) + +// eventLogReceiver fulfills the seelog.CustomReceiver interface +type eventLogReceiver struct{} + +var eventLog *eventlog.Log + +func init() { + eventlog.InstallAsEventCreate(eventLogName, windows.EVENTLOG_INFORMATION_TYPE|windows.EVENTLOG_WARNING_TYPE|windows.EVENTLOG_ERROR_TYPE) + var err error + eventLog, err = eventlog.Open(eventLogName) + if err != nil { + panic(err) + } +} + +// registerPlatformLogger registers the eventLogReceiver +func registerPlatformLogger() { + seelog.RegisterReceiver("wineventlog", &eventLogReceiver{}) +} + +// platformLogConfig exposes log configuration for the event log receiver +func platformLogConfig() string { + return `` +} + +// ReceiveMessage receives a log line from seelog and emits it to the Windows event log +func (r *eventLogReceiver) ReceiveMessage(message string, level seelog.LogLevel, context seelog.LogContextInterface) error { + switch level { + case seelog.DebugLvl, seelog.InfoLvl: + return eventLog.Info(eventLogID, message) + case seelog.WarnLvl: + return eventLog.Warning(eventLogID, message) + case seelog.ErrorLvl, seelog.CriticalLvl: + return eventLog.Error(eventLogID, message) + } + return nil +} + +func (r *eventLogReceiver) AfterParse(initArgs seelog.CustomReceiverInitArgs) error { return nil } +func (r *eventLogReceiver) Flush() {} +func (r *eventLogReceiver) Close() error { return nil } diff --git a/agent/logger/log.go b/agent/logger/log.go index 9039442b5c4..0a1867c5cf8 100644 --- a/agent/logger/log.go +++ b/agent/logger/log.go @@ -59,6 +59,7 @@ func initLogger() { logfile = os.Getenv(LOGFILE_ENV_VAR) SetLevel(envLevel) + registerPlatformLogger() reloadConfig() } @@ -86,7 +87,7 @@ func SetLevel(logLevel string) { // GetLevel gets the log level func GetLevel() string { levelLock.RLock() - defer levelLock.RLock() + defer levelLock.RUnlock() return level } diff --git a/agent/logger/platform_unix.go b/agent/logger/platform_unix.go new file mode 100644 index 00000000000..cab8d236f5d --- /dev/null +++ b/agent/logger/platform_unix.go @@ -0,0 +1,22 @@ +// +build !windows + +// Copyright 2017 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 logger + +// registerPlatformLogger does nothing on Linux +func registerPlatformLogger() {} + +// platformLogConfig does nothing on Linux +func platformLogConfig() string { return "" } diff --git a/agent/logger/seelog_config.go b/agent/logger/seelog_config.go index 43b87b62703..3211a115cc1 100644 --- a/agent/logger/seelog_config.go +++ b/agent/logger/seelog_config.go @@ -18,6 +18,7 @@ func loggerConfig() string { ` + config += platformLogConfig() if logfile != "" { config += `` @@ -26,6 +27,7 @@ func loggerConfig() string { + ` diff --git a/agent/sighandlers/termination_handler.go b/agent/sighandlers/termination_handler.go index c2b4afa2efb..b7a5f34863c 100644 --- a/agent/sighandlers/termination_handler.go +++ b/agent/sighandlers/termination_handler.go @@ -11,7 +11,7 @@ // express or implied. See the License for the specific language governing // permissions and limitations under the License. -// sighandlers handle signals and behave appropriately. +// Package sighandlers handle signals and behave appropriately. // SIGTERM: // Flush state to disk and exit // SIGUSR1: @@ -26,33 +26,38 @@ import ( "time" "github.com/aws/amazon-ecs-agent/agent/engine" - "github.com/aws/amazon-ecs-agent/agent/logger" "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" "github.com/aws/amazon-ecs-agent/agent/statemanager" "github.com/aws/amazon-ecs-agent/agent/utils" + + "github.com/cihub/seelog" +) + +const ( + engineDisableTimeout = 5 * time.Second + finalSaveTimeout = 3 * time.Second ) -var log = logger.ForModule("TerminationHandler") +// TerminationHandler defines a handler used for terminating the agent +type TerminationHandler func(saver statemanager.Saver, taskEngine engine.TaskEngine) -func StartTerminationHandler(saver statemanager.Saver, taskEngine engine.TaskEngine) { +// StartDefaultTerminationHandler defines a default termination handler suitable for running in a process +func StartDefaultTerminationHandler(saver statemanager.Saver, taskEngine engine.TaskEngine) { signalChannel := make(chan os.Signal, 2) signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) sig := <-signalChannel - log.Debug("Received termination signal", "signal", sig.String()) + seelog.Debugf("Termination handler received termination signal: %s", sig.String()) err := FinalSave(saver, taskEngine) if err != nil { - log.Crit("Error saving state before final shutdown", "err", err) + seelog.Criticalf("Error saving state before final shutdown: %v", err) // Terminal because it's a sigterm; the user doesn't want it to restart os.Exit(exitcodes.ExitTerminal) } os.Exit(exitcodes.ExitSuccess) } -const engineDisableTimeout = 5 * time.Second -const finalSaveTimeout = 3 * time.Second - // FinalSave should be called immediately before exiting, and only before // exiting, in order to flush tasks to disk. It waits a short timeout for state // to settle if necessary. If unable to reach a steady-state and save within @@ -61,11 +66,11 @@ func FinalSave(saver statemanager.Saver, taskEngine engine.TaskEngine) error { engineDisabled := make(chan error) disableTimer := time.AfterFunc(engineDisableTimeout, func() { - engineDisabled <- errors.New("Timed out waiting for TaskEngine to settle") + engineDisabled <- errors.New("final save: timed out waiting for TaskEngine to settle") }) go func() { - log.Debug("Shutting down task engine") + seelog.Debug("Shutting down task engine for final save") taskEngine.Disable() disableTimer.Stop() engineDisabled <- nil @@ -75,10 +80,10 @@ func FinalSave(saver statemanager.Saver, taskEngine engine.TaskEngine) error { stateSaved := make(chan error) saveTimer := time.AfterFunc(finalSaveTimeout, func() { - stateSaved <- errors.New("Timed out trying to save to disk") + stateSaved <- errors.New("final save: timed out trying to save to disk") }) go func() { - log.Debug("Saving state before shutting down") + seelog.Debug("Saving state before shutting down") stateSaved <- saver.ForceSave() saveTimer.Stop() }() diff --git a/agent/stats/container_test.go b/agent/stats/container_test.go index d74a5e66c19..00f4bd51163 100644 --- a/agent/stats/container_test.go +++ b/agent/stats/container_test.go @@ -61,14 +61,14 @@ func TestContainerStatsCollection(t *testing.T) { // deal with the docker.Stats.MemoryStats inner struct jsonStat := fmt.Sprintf(` { - "memory_stats": {"usage":%d}, + "memory_stats": {"usage":%d, "privateworkingset":%d}, "cpu_stats":{ "cpu_usage":{ "percpu_usage":[%d], "total_usage":%d } } - }`, stat.memBytes, stat.cpuTime, stat.cpuTime) + }`, stat.memBytes, stat.memBytes, stat.cpuTime, stat.cpuTime) dockerStat := &docker.Stats{} json.Unmarshal([]byte(jsonStat), dockerStat) dockerStat.Read = stat.timestamp diff --git a/agent/stats/utils.go b/agent/stats/utils.go index 526092798ef..e2ddd17a007 100644 --- a/agent/stats/utils.go +++ b/agent/stats/utils.go @@ -14,14 +14,12 @@ package stats import ( - "fmt" "math" "regexp" "runtime" "time" "github.com/cihub/seelog" - docker "github.com/fsouza/go-dockerclient" ) // networkStatsErrorPattern defines the pattern that is used to evaluate @@ -35,23 +33,6 @@ func nan32() float32 { return (float32)(math.NaN()) } -// dockerStatsToContainerStats returns a new object of the ContainerStats object from docker stats. -func dockerStatsToContainerStats(dockerStats *docker.Stats) (*ContainerStats, error) { - // The length of PercpuUsage represents the number of cores in an instance. - if len(dockerStats.CPUStats.CPUUsage.PercpuUsage) == 0 { - seelog.Debug("Invalid container statistics reported, invalid stats payload from docker") - return nil, fmt.Errorf("Invalid container statistics reported") - } - - cpuUsage := dockerStats.CPUStats.CPUUsage.TotalUsage / numCores - memoryUsage := dockerStats.MemoryStats.Usage - dockerStats.MemoryStats.Stats.Cache - return &ContainerStats{ - cpuUsage: cpuUsage, - memoryUsage: memoryUsage, - timestamp: dockerStats.Read, - }, nil -} - // parseNanoTime returns the time object from a string formatted with RFC3339Nano layout. func parseNanoTime(value string) time.Time { ts, _ := time.Parse(time.RFC3339Nano, value) diff --git a/agent/stats/utils_test.go b/agent/stats/utils_test.go index 7daf704885e..de62f0ee791 100644 --- a/agent/stats/utils_test.go +++ b/agent/stats/utils_test.go @@ -66,25 +66,6 @@ func TestDockerStatsToContainerStatsCpuUsage(t *testing.T) { } } -func TestDockerStatsToContainerStatsZeroCoresGeneratesError(t *testing.T) { - // doing this with json makes me sad, but is the easiest way to deal with - // the inner structs - jsonStat := fmt.Sprintf(` - { - "cpu_stats":{ - "cpu_usage":{ - "total_usage":%d - } - } - }`, 100) - dockerStat := &docker.Stats{} - json.Unmarshal([]byte(jsonStat), dockerStat) - _, err := dockerStatsToContainerStats(dockerStat) - if err == nil { - t.Error("Expected error converting container stats with empty PercpuUsage") - } -} - func TestDockerStatsToContainerStatsMemUsage(t *testing.T) { jsonStat := fmt.Sprintf(` { @@ -100,9 +81,10 @@ func TestDockerStatsToContainerStatsMemUsage(t *testing.T) { "stats": { "cache": %d, "rss": %d - } + }, + "privateworkingset": %d } - }`, 1, 2, 3, 4, 100, 30, 100, 20, 10) + }`, 1, 2, 3, 4, 100, 30, 100, 20, 10, 10) dockerStat := &docker.Stats{} json.Unmarshal([]byte(jsonStat), dockerStat) containerStats, err := dockerStatsToContainerStats(dockerStat) diff --git a/agent/stats/utils_unix.go b/agent/stats/utils_unix.go new file mode 100644 index 00000000000..e51c4a7cff0 --- /dev/null +++ b/agent/stats/utils_unix.go @@ -0,0 +1,39 @@ +// +build !windows +// Copyright 2014-2017 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 stats + +import ( + "fmt" + + "github.com/cihub/seelog" + docker "github.com/fsouza/go-dockerclient" +) + +// dockerStatsToContainerStats returns a new object of the ContainerStats object from docker stats. +func dockerStatsToContainerStats(dockerStats *docker.Stats) (*ContainerStats, error) { + // The length of PercpuUsage represents the number of cores in an instance. + if len(dockerStats.CPUStats.CPUUsage.PercpuUsage) == 0 || numCores == uint64(0) { + seelog.Debug("Invalid container statistics reported, no cpu core usage reported") + return nil, fmt.Errorf("Invalid container statistics reported, no cpu core usage reported") + } + + cpuUsage := dockerStats.CPUStats.CPUUsage.TotalUsage / numCores + memoryUsage := dockerStats.MemoryStats.Usage - dockerStats.MemoryStats.Stats.Cache + return &ContainerStats{ + cpuUsage: cpuUsage, + memoryUsage: memoryUsage, + timestamp: dockerStats.Read, + }, nil +} diff --git a/agent/stats/utils_unix_test.go b/agent/stats/utils_unix_test.go new file mode 100644 index 00000000000..7191efbd17e --- /dev/null +++ b/agent/stats/utils_unix_test.go @@ -0,0 +1,42 @@ +// +build !windows,!integration + +// Copyright 2014-2016 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 stats + +import ( + "encoding/json" + "fmt" + "testing" + + docker "github.com/fsouza/go-dockerclient" +) + +func TestDockerStatsToContainerStatsZeroCoresGeneratesError(t *testing.T) { + // doing this with json makes me sad, but is the easiest way to deal with + // the inner structs + jsonStat := fmt.Sprintf(` + { + "cpu_stats":{ + "cpu_usage":{ + "total_usage":%d + } + } + }`, 100) + dockerStat := &docker.Stats{} + json.Unmarshal([]byte(jsonStat), dockerStat) + _, err := dockerStatsToContainerStats(dockerStat) + if err == nil { + t.Error("Expected error converting container stats with empty PercpuUsage") + } +} diff --git a/agent/stats/utils_windows.go b/agent/stats/utils_windows.go new file mode 100644 index 00000000000..45f8897b649 --- /dev/null +++ b/agent/stats/utils_windows.go @@ -0,0 +1,38 @@ +// +build windows +// Copyright 2014-2017 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 stats + +import ( + "fmt" + + "github.com/cihub/seelog" + docker "github.com/fsouza/go-dockerclient" +) + +// dockerStatsToContainerStats returns a new object of the ContainerStats object from docker stats. +func dockerStatsToContainerStats(dockerStats *docker.Stats) (*ContainerStats, error) { + if numCores == uint64(0) { + seelog.Error("Invalid number of cpu cores acquired from the system") + return nil, fmt.Errorf("invalid number of cpu cores acquired from the system") + } + + cpuUsage := dockerStats.CPUStats.CPUUsage.TotalUsage / numCores + memoryUsage := dockerStats.MemoryStats.PrivateWorkingSet + return &ContainerStats{ + cpuUsage: cpuUsage, + memoryUsage: memoryUsage, + timestamp: dockerStats.Read, + }, nil +} diff --git a/agent/stats/utils_windows_test.go b/agent/stats/utils_windows_test.go new file mode 100644 index 00000000000..42a7c94a4e7 --- /dev/null +++ b/agent/stats/utils_windows_test.go @@ -0,0 +1,41 @@ +// +build windows,!integration + +// Copyright 2017 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 stats + +import ( + "encoding/json" + "fmt" + "testing" + + docker "github.com/fsouza/go-dockerclient" +) + +func TestDockerStatsToContainerStatsZeroCoresGeneratesError(t *testing.T) { + numCores = uint64(0) + jsonStat := fmt.Sprintf(` + { + "cpu_stats":{ + "cpu_usage":{ + "total_usage":%d + } + } + }`, 100) + dockerStat := &docker.Stats{} + json.Unmarshal([]byte(jsonStat), dockerStat) + _, err := dockerStatsToContainerStats(dockerStat) + if err == nil { + t.Error("Expected error converting container stats with zero cpu cores") + } +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/event.go b/agent/vendor/golang.org/x/sys/windows/svc/event.go new file mode 100644 index 00000000000..0508e228818 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/event.go @@ -0,0 +1,48 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc + +import ( + "errors" + + "golang.org/x/sys/windows" +) + +// event represents auto-reset, initially non-signaled Windows event. +// It is used to communicate between go and asm parts of this package. +type event struct { + h windows.Handle +} + +func newEvent() (*event, error) { + h, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return nil, err + } + return &event{h: h}, nil +} + +func (e *event) Close() error { + return windows.CloseHandle(e.h) +} + +func (e *event) Set() error { + return windows.SetEvent(e.h) +} + +func (e *event) Wait() error { + s, err := windows.WaitForSingleObject(e.h, windows.INFINITE) + switch s { + case windows.WAIT_OBJECT_0: + break + case windows.WAIT_FAILED: + return err + default: + return errors.New("unexpected result from WaitForSingleObject") + } + return nil +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go new file mode 100644 index 00000000000..c76a3760a42 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go @@ -0,0 +1,80 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package eventlog + +import ( + "errors" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +const ( + // Log levels. + Info = windows.EVENTLOG_INFORMATION_TYPE + Warning = windows.EVENTLOG_WARNING_TYPE + Error = windows.EVENTLOG_ERROR_TYPE +) + +const addKeyName = `SYSTEM\CurrentControlSet\Services\EventLog\Application` + +// Install modifies PC registry to allow logging with an event source src. +// It adds all required keys and values to the event log registry key. +// Install uses msgFile as the event message file. If useExpandKey is true, +// the event message file is installed as REG_EXPAND_SZ value, +// otherwise as REG_SZ. Use bitwise of log.Error, log.Warning and +// log.Info to specify events supported by the new event source. +func Install(src, msgFile string, useExpandKey bool, eventsSupported uint32) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.CREATE_SUB_KEY) + if err != nil { + return err + } + defer appkey.Close() + + sk, alreadyExist, err := registry.CreateKey(appkey, src, registry.SET_VALUE) + if err != nil { + return err + } + defer sk.Close() + if alreadyExist { + return errors.New(addKeyName + `\` + src + " registry key already exists") + } + + err = sk.SetDWordValue("CustomSource", 1) + if err != nil { + return err + } + if useExpandKey { + err = sk.SetExpandStringValue("EventMessageFile", msgFile) + } else { + err = sk.SetStringValue("EventMessageFile", msgFile) + } + if err != nil { + return err + } + err = sk.SetDWordValue("TypesSupported", eventsSupported) + if err != nil { + return err + } + return nil +} + +// InstallAsEventCreate is the same as Install, but uses +// %SystemRoot%\System32\EventCreate.exe as the event message file. +func InstallAsEventCreate(src string, eventsSupported uint32) error { + return Install(src, "%SystemRoot%\\System32\\EventCreate.exe", true, eventsSupported) +} + +// Remove deletes all registry elements installed by the correspondent Install. +func Remove(src string) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.SET_VALUE) + if err != nil { + return err + } + defer appkey.Close() + return registry.DeleteKey(appkey, src) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go new file mode 100644 index 00000000000..46e5153d024 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go @@ -0,0 +1,70 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package eventlog implements access to Windows event log. +// +package eventlog + +import ( + "errors" + "syscall" + + "golang.org/x/sys/windows" +) + +// Log provides access to the system log. +type Log struct { + Handle windows.Handle +} + +// Open retrieves a handle to the specified event log. +func Open(source string) (*Log, error) { + return OpenRemote("", source) +} + +// OpenRemote does the same as Open, but on different computer host. +func OpenRemote(host, source string) (*Log, error) { + if source == "" { + return nil, errors.New("Specify event log source") + } + var s *uint16 + if host != "" { + s = syscall.StringToUTF16Ptr(host) + } + h, err := windows.RegisterEventSource(s, syscall.StringToUTF16Ptr(source)) + if err != nil { + return nil, err + } + return &Log{Handle: h}, nil +} + +// Close closes event log l. +func (l *Log) Close() error { + return windows.DeregisterEventSource(l.Handle) +} + +func (l *Log) report(etype uint16, eid uint32, msg string) error { + ss := []*uint16{syscall.StringToUTF16Ptr(msg)} + return windows.ReportEvent(l.Handle, etype, 0, eid, 0, 1, 0, &ss[0], nil) +} + +// Info writes an information event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Info(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_INFORMATION_TYPE, eid, msg) +} + +// Warning writes an warning event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Warning(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_WARNING_TYPE, eid, msg) +} + +// Error writes an error event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Error(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_ERROR_TYPE, eid, msg) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go new file mode 100644 index 00000000000..6fbbd4a8764 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go @@ -0,0 +1,51 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package eventlog_test + +import ( + "testing" + + "golang.org/x/sys/windows/svc/eventlog" +) + +func TestLog(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode - it modifies system logs") + } + + const name = "mylog" + const supports = eventlog.Error | eventlog.Warning | eventlog.Info + err := eventlog.InstallAsEventCreate(name, supports) + if err != nil { + t.Fatalf("Install failed: %s", err) + } + defer func() { + err = eventlog.Remove(name) + if err != nil { + t.Fatalf("Remove failed: %s", err) + } + }() + + l, err := eventlog.Open(name) + if err != nil { + t.Fatalf("Open failed: %s", err) + } + defer l.Close() + + err = l.Info(1, "info") + if err != nil { + t.Fatalf("Info failed: %s", err) + } + err = l.Warning(2, "warning") + if err != nil { + t.Fatalf("Warning failed: %s", err) + } + err = l.Error(3, "error") + if err != nil { + t.Fatalf("Error failed: %s", err) + } +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go12.c b/agent/vendor/golang.org/x/sys/windows/svc/go12.c new file mode 100644 index 00000000000..6f1be1fa3bc --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go12.c @@ -0,0 +1,24 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build !go1.3 + +// copied from pkg/runtime +typedef unsigned int uint32; +typedef unsigned long long int uint64; +#ifdef _64BIT +typedef uint64 uintptr; +#else +typedef uint32 uintptr; +#endif + +// from sys_386.s or sys_amd64.s +void ·servicemain(void); + +void +·getServiceMain(uintptr *r) +{ + *r = (uintptr)·servicemain; +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go12.go b/agent/vendor/golang.org/x/sys/windows/svc/go12.go new file mode 100644 index 00000000000..cd8b913c99d --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go12.go @@ -0,0 +1,11 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build !go1.3 + +package svc + +// from go12.c +func getServiceMain(r *uintptr) diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go13.go b/agent/vendor/golang.org/x/sys/windows/svc/go13.go new file mode 100644 index 00000000000..9d7f3cec54c --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go13.go @@ -0,0 +1,31 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build go1.3 + +package svc + +import "unsafe" + +const ptrSize = 4 << (^uintptr(0) >> 63) // unsafe.Sizeof(uintptr(0)) but an ideal const + +// Should be a built-in for unsafe.Pointer? +func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(p) + x) +} + +// funcPC returns the entry PC of the function f. +// It assumes that f is a func value. Otherwise the behavior is undefined. +func funcPC(f interface{}) uintptr { + return **(**uintptr)(add(unsafe.Pointer(&f), ptrSize)) +} + +// from sys_386.s and sys_amd64.s +func servicectlhandler(ctl uint32) uintptr +func servicemain(argc uint32, argv **uint16) + +func getServiceMain(r *uintptr) { + *r = funcPC(servicemain) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/security.go b/agent/vendor/golang.org/x/sys/windows/svc/security.go new file mode 100644 index 00000000000..6fbc9236ed5 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/security.go @@ -0,0 +1,62 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func allocSid(subAuth0 uint32) (*windows.SID, error) { + var sid *windows.SID + err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, + 1, subAuth0, 0, 0, 0, 0, 0, 0, 0, &sid) + if err != nil { + return nil, err + } + return sid, nil +} + +// IsAnInteractiveSession determines if calling process is running interactively. +// It queries the process token for membership in the Interactive group. +// http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s +func IsAnInteractiveSession() (bool, error) { + interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(interSid) + + serviceSid, err := allocSid(windows.SECURITY_SERVICE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(serviceSid) + + t, err := windows.OpenCurrentProcessToken() + if err != nil { + return false, err + } + defer t.Close() + + gs, err := t.GetTokenGroups() + if err != nil { + return false, err + } + p := unsafe.Pointer(&gs.Groups[0]) + groups := (*[2 << 20]windows.SIDAndAttributes)(p)[:gs.GroupCount] + for _, g := range groups { + if windows.EqualSid(g.Sid, interSid) { + return true, nil + } + if windows.EqualSid(g.Sid, serviceSid) { + return false, nil + } + } + return false, nil +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/service.go b/agent/vendor/golang.org/x/sys/windows/svc/service.go new file mode 100644 index 00000000000..903cba3f121 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/service.go @@ -0,0 +1,363 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package svc provides everything required to build Windows service. +// +package svc + +import ( + "errors" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// State describes service execution state (Stopped, Running and so on). +type State uint32 + +const ( + Stopped = State(windows.SERVICE_STOPPED) + StartPending = State(windows.SERVICE_START_PENDING) + StopPending = State(windows.SERVICE_STOP_PENDING) + Running = State(windows.SERVICE_RUNNING) + ContinuePending = State(windows.SERVICE_CONTINUE_PENDING) + PausePending = State(windows.SERVICE_PAUSE_PENDING) + Paused = State(windows.SERVICE_PAUSED) +) + +// Cmd represents service state change request. It is sent to a service +// by the service manager, and should be actioned upon by the service. +type Cmd uint32 + +const ( + Stop = Cmd(windows.SERVICE_CONTROL_STOP) + Pause = Cmd(windows.SERVICE_CONTROL_PAUSE) + Continue = Cmd(windows.SERVICE_CONTROL_CONTINUE) + Interrogate = Cmd(windows.SERVICE_CONTROL_INTERROGATE) + Shutdown = Cmd(windows.SERVICE_CONTROL_SHUTDOWN) + ParamChange = Cmd(windows.SERVICE_CONTROL_PARAMCHANGE) + NetBindAdd = Cmd(windows.SERVICE_CONTROL_NETBINDADD) + NetBindRemove = Cmd(windows.SERVICE_CONTROL_NETBINDREMOVE) + NetBindEnable = Cmd(windows.SERVICE_CONTROL_NETBINDENABLE) + NetBindDisable = Cmd(windows.SERVICE_CONTROL_NETBINDDISABLE) + DeviceEvent = Cmd(windows.SERVICE_CONTROL_DEVICEEVENT) + HardwareProfileChange = Cmd(windows.SERVICE_CONTROL_HARDWAREPROFILECHANGE) + PowerEvent = Cmd(windows.SERVICE_CONTROL_POWEREVENT) + SessionChange = Cmd(windows.SERVICE_CONTROL_SESSIONCHANGE) +) + +// Accepted is used to describe commands accepted by the service. +// Note that Interrogate is always accepted. +type Accepted uint32 + +const ( + AcceptStop = Accepted(windows.SERVICE_ACCEPT_STOP) + AcceptShutdown = Accepted(windows.SERVICE_ACCEPT_SHUTDOWN) + AcceptPauseAndContinue = Accepted(windows.SERVICE_ACCEPT_PAUSE_CONTINUE) + AcceptParamChange = Accepted(windows.SERVICE_ACCEPT_PARAMCHANGE) + AcceptNetBindChange = Accepted(windows.SERVICE_ACCEPT_NETBINDCHANGE) + AcceptHardwareProfileChange = Accepted(windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE) + AcceptPowerEvent = Accepted(windows.SERVICE_ACCEPT_POWEREVENT) + AcceptSessionChange = Accepted(windows.SERVICE_ACCEPT_SESSIONCHANGE) +) + +// Status combines State and Accepted commands to fully describe running service. +type Status struct { + State State + Accepts Accepted + CheckPoint uint32 // used to report progress during a lengthy operation + WaitHint uint32 // estimated time required for a pending operation, in milliseconds +} + +// ChangeRequest is sent to the service Handler to request service status change. +type ChangeRequest struct { + Cmd Cmd + EventType uint32 + EventData uintptr + CurrentStatus Status +} + +// Handler is the interface that must be implemented to build Windows service. +type Handler interface { + + // Execute will be called by the package code at the start of + // the service, and the service will exit once Execute completes. + // Inside Execute you must read service change requests from r and + // act accordingly. You must keep service control manager up to date + // about state of your service by writing into s as required. + // args contains service name followed by argument strings passed + // to the service. + // You can provide service exit code in exitCode return parameter, + // with 0 being "no error". You can also indicate if exit code, + // if any, is service specific or not by using svcSpecificEC + // parameter. + Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32) +} + +var ( + // These are used by asm code. + goWaitsH uintptr + cWaitsH uintptr + ssHandle uintptr + sName *uint16 + sArgc uintptr + sArgv **uint16 + ctlHandlerExProc uintptr + cSetEvent uintptr + cWaitForSingleObject uintptr + cRegisterServiceCtrlHandlerExW uintptr +) + +func init() { + k := syscall.MustLoadDLL("kernel32.dll") + cSetEvent = k.MustFindProc("SetEvent").Addr() + cWaitForSingleObject = k.MustFindProc("WaitForSingleObject").Addr() + a := syscall.MustLoadDLL("advapi32.dll") + cRegisterServiceCtrlHandlerExW = a.MustFindProc("RegisterServiceCtrlHandlerExW").Addr() +} + +// The HandlerEx prototype also has a context pointer but since we don't use +// it at start-up time we don't have to pass it over either. +type ctlEvent struct { + cmd Cmd + eventType uint32 + eventData uintptr + errno uint32 +} + +// service provides access to windows service api. +type service struct { + name string + h windows.Handle + cWaits *event + goWaits *event + c chan ctlEvent + handler Handler +} + +func newService(name string, handler Handler) (*service, error) { + var s service + var err error + s.name = name + s.c = make(chan ctlEvent) + s.handler = handler + s.cWaits, err = newEvent() + if err != nil { + return nil, err + } + s.goWaits, err = newEvent() + if err != nil { + s.cWaits.Close() + return nil, err + } + return &s, nil +} + +func (s *service) close() error { + s.cWaits.Close() + s.goWaits.Close() + return nil +} + +type exitCode struct { + isSvcSpecific bool + errno uint32 +} + +func (s *service) updateStatus(status *Status, ec *exitCode) error { + if s.h == 0 { + return errors.New("updateStatus with no service status handle") + } + var t windows.SERVICE_STATUS + t.ServiceType = windows.SERVICE_WIN32_OWN_PROCESS + t.CurrentState = uint32(status.State) + if status.Accepts&AcceptStop != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_STOP + } + if status.Accepts&AcceptShutdown != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_SHUTDOWN + } + if status.Accepts&AcceptPauseAndContinue != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_PAUSE_CONTINUE + } + if status.Accepts&AcceptParamChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_PARAMCHANGE + } + if status.Accepts&AcceptNetBindChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_NETBINDCHANGE + } + if status.Accepts&AcceptHardwareProfileChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE + } + if status.Accepts&AcceptPowerEvent != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_POWEREVENT + } + if status.Accepts&AcceptSessionChange != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_SESSIONCHANGE + } + if ec.errno == 0 { + t.Win32ExitCode = windows.NO_ERROR + t.ServiceSpecificExitCode = windows.NO_ERROR + } else if ec.isSvcSpecific { + t.Win32ExitCode = uint32(windows.ERROR_SERVICE_SPECIFIC_ERROR) + t.ServiceSpecificExitCode = ec.errno + } else { + t.Win32ExitCode = ec.errno + t.ServiceSpecificExitCode = windows.NO_ERROR + } + t.CheckPoint = status.CheckPoint + t.WaitHint = status.WaitHint + return windows.SetServiceStatus(s.h, &t) +} + +const ( + sysErrSetServiceStatusFailed = uint32(syscall.APPLICATION_ERROR) + iota + sysErrNewThreadInCallback +) + +func (s *service) run() { + s.goWaits.Wait() + s.h = windows.Handle(ssHandle) + argv := (*[100]*int16)(unsafe.Pointer(sArgv))[:sArgc] + args := make([]string, len(argv)) + for i, a := range argv { + args[i] = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(a))[:]) + } + + cmdsToHandler := make(chan ChangeRequest) + changesFromHandler := make(chan Status) + exitFromHandler := make(chan exitCode) + + go func() { + ss, errno := s.handler.Execute(args, cmdsToHandler, changesFromHandler) + exitFromHandler <- exitCode{ss, errno} + }() + + status := Status{State: Stopped} + ec := exitCode{isSvcSpecific: true, errno: 0} + var outch chan ChangeRequest + inch := s.c + var cmd Cmd + var evtype uint32 + var evdata uintptr +loop: + for { + select { + case r := <-inch: + if r.errno != 0 { + ec.errno = r.errno + break loop + } + inch = nil + outch = cmdsToHandler + cmd = r.cmd + evtype = r.eventType + evdata = r.eventData + case outch <- ChangeRequest{cmd, evtype, evdata, status}: + inch = s.c + outch = nil + case c := <-changesFromHandler: + err := s.updateStatus(&c, &ec) + if err != nil { + // best suitable error number + ec.errno = sysErrSetServiceStatusFailed + if err2, ok := err.(syscall.Errno); ok { + ec.errno = uint32(err2) + } + break loop + } + status = c + case ec = <-exitFromHandler: + break loop + } + } + + s.updateStatus(&Status{State: Stopped}, &ec) + s.cWaits.Set() +} + +func newCallback(fn interface{}) (cb uintptr, err error) { + defer func() { + r := recover() + if r == nil { + return + } + cb = 0 + switch v := r.(type) { + case string: + err = errors.New(v) + case error: + err = v + default: + err = errors.New("unexpected panic in syscall.NewCallback") + } + }() + return syscall.NewCallback(fn), nil +} + +// BUG(brainman): There is no mechanism to run multiple services +// inside one single executable. Perhaps, it can be overcome by +// using RegisterServiceCtrlHandlerEx Windows api. + +// Run executes service name by calling appropriate handler function. +func Run(name string, handler Handler) error { + runtime.LockOSThread() + + tid := windows.GetCurrentThreadId() + + s, err := newService(name, handler) + if err != nil { + return err + } + + ctlHandler := func(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr { + e := ctlEvent{cmd: Cmd(ctl), eventType: evtype, eventData: evdata} + // We assume that this callback function is running on + // the same thread as Run. Nowhere in MS documentation + // I could find statement to guarantee that. So putting + // check here to verify, otherwise things will go bad + // quickly, if ignored. + i := windows.GetCurrentThreadId() + if i != tid { + e.errno = sysErrNewThreadInCallback + } + s.c <- e + // Always return NO_ERROR (0) for now. + return 0 + } + + var svcmain uintptr + getServiceMain(&svcmain) + t := []windows.SERVICE_TABLE_ENTRY{ + {syscall.StringToUTF16Ptr(s.name), svcmain}, + {nil, 0}, + } + + goWaitsH = uintptr(s.goWaits.h) + cWaitsH = uintptr(s.cWaits.h) + sName = t[0].ServiceName + ctlHandlerExProc, err = newCallback(ctlHandler) + if err != nil { + return err + } + + go s.run() + + err = windows.StartServiceCtrlDispatcher(&t[0]) + if err != nil { + return err + } + return nil +} + +// StatusHandle returns service status handle. It is safe to call this function +// from inside the Handler.Execute because then it is guaranteed to be set. +// This code will have to change once multiple services are possible per process. +func StatusHandle() windows.Handle { + return windows.Handle(ssHandle) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go b/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go new file mode 100644 index 00000000000..da7ec666484 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go @@ -0,0 +1,118 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +func getState(t *testing.T, s *mgr.Service) svc.State { + status, err := s.Query() + if err != nil { + t.Fatalf("Query(%s) failed: %s", s.Name, err) + } + return status.State +} + +func testState(t *testing.T, s *mgr.Service, want svc.State) { + have := getState(t, s) + if have != want { + t.Fatalf("%s state is=%d want=%d", s.Name, have, want) + } +} + +func waitState(t *testing.T, s *mgr.Service, want svc.State) { + for i := 0; ; i++ { + have := getState(t, s) + if have == want { + return + } + if i > 10 { + t.Fatalf("%s state is=%d, waiting timeout", s.Name, have) + } + time.Sleep(300 * time.Millisecond) + } +} + +func TestExample(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode - it modifies system services") + } + + const name = "myservice" + + m, err := mgr.Connect() + if err != nil { + t.Fatalf("SCM connection failed: %s", err) + } + defer m.Disconnect() + + dir, err := ioutil.TempDir("", "svc") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + defer os.RemoveAll(dir) + + exepath := filepath.Join(dir, "a.exe") + o, err := exec.Command("go", "build", "-o", exepath, "golang.org/x/sys/windows/svc/example").CombinedOutput() + if err != nil { + t.Fatalf("failed to build service program: %v\n%v", err, string(o)) + } + + s, err := m.OpenService(name) + if err == nil { + err = s.Delete() + if err != nil { + s.Close() + t.Fatalf("Delete failed: %s", err) + } + s.Close() + } + s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: "my service"}, "is", "auto-started") + if err != nil { + t.Fatalf("CreateService(%s) failed: %v", name, err) + } + defer s.Close() + + testState(t, s, svc.Stopped) + err = s.Start("is", "manual-started") + if err != nil { + t.Fatalf("Start(%s) failed: %s", s.Name, err) + } + waitState(t, s, svc.Running) + time.Sleep(1 * time.Second) + + // testing deadlock from issues 4. + _, err = s.Control(svc.Interrogate) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + _, err = s.Control(svc.Interrogate) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + time.Sleep(1 * time.Second) + + _, err = s.Control(svc.Stop) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + waitState(t, s, svc.Stopped) + + err = s.Delete() + if err != nil { + t.Fatalf("Delete failed: %s", err) + } +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s b/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s new file mode 100644 index 00000000000..2c82a9d91d7 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s @@ -0,0 +1,68 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL argc+0(FP), AX + MOVL AX, ·sArgc(SB) + MOVL argv+4(FP), AX + MOVL AX, ·sArgv(SB) + + PUSHL BP + PUSHL BX + PUSHL SI + PUSHL DI + + SUBL $12, SP + + MOVL ·sName(SB), AX + MOVL AX, (SP) + MOVL $·servicectlhandler(SB), AX + MOVL AX, 4(SP) + MOVL $0, 8(SP) + MOVL ·cRegisterServiceCtrlHandlerExW(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + CMPL AX, $0 + JE exit + MOVL AX, ·ssHandle(SB) + + MOVL ·goWaitsH(SB), AX + MOVL AX, (SP) + MOVL ·cSetEvent(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + + MOVL ·cWaitsH(SB), AX + MOVL AX, (SP) + MOVL $-1, AX + MOVL AX, 4(SP) + MOVL ·cWaitForSingleObject(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + +exit: + ADDL $12, SP + + POPL DI + POPL SI + POPL BX + POPL BP + + MOVL 0(SP), CX + ADDL $12, SP + JMP CX + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr { +TEXT ·servicectlhandler(SB),7,$0 + MOVL ·ctlHandlerExProc(SB), CX + JMP CX diff --git a/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s b/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s new file mode 100644 index 00000000000..06b425900df --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s @@ -0,0 +1,42 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL CX, ·sArgc(SB) + MOVL DX, ·sArgv(SB) + + SUBQ $32, SP // stack for the first 4 syscall params + + MOVQ ·sName(SB), CX + MOVQ $·servicectlhandler(SB), DX + // BUG(pastarmovj): Figure out a way to pass in context in R8. + MOVQ ·cRegisterServiceCtrlHandlerExW(SB), AX + CALL AX + CMPQ AX, $0 + JE exit + MOVQ AX, ·ssHandle(SB) + + MOVQ ·goWaitsH(SB), CX + MOVQ ·cSetEvent(SB), AX + CALL AX + + MOVQ ·cWaitsH(SB), CX + MOVQ $4294967295, DX + MOVQ ·cWaitForSingleObject(SB), AX + CALL AX + +exit: + ADDQ $32, SP + RET + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func ·servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr { +TEXT ·servicectlhandler(SB),7,$0 + MOVQ ·ctlHandlerExProc(SB), AX + JMP AX diff --git a/misc/windows-cpupercent/build.ps1 b/misc/windows-cpupercent/build.ps1 new file mode 100644 index 00000000000..529f3dcef95 --- /dev/null +++ b/misc/windows-cpupercent/build.ps1 @@ -0,0 +1,14 @@ +# Copyright 2017 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. + +docker build -t "amazon/amazon-ecs-windows-cpupercent-test:make" -f "${PSScriptRoot}/windows.dockerfile" ${PSScriptRoot} diff --git a/misc/windows-cpupercent/main.go b/misc/windows-cpupercent/main.go new file mode 100644 index 00000000000..3b04483f2d2 --- /dev/null +++ b/misc/windows-cpupercent/main.go @@ -0,0 +1,37 @@ +// Copyright 2017 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 main + +import ( + "crypto/md5" + "flag" + "fmt" +) + +func main() { + concurrency := flag.Int("concurrency", 1, "amount of concurrency") + flag.Parse() + neverdie := make(chan struct{}) + + fmt.Printf("Hogging CPU with concurrency %d\n", *concurrency) + for i := 0; i < *concurrency; i++ { + go func() { + md5hash := md5.New() + for { + md5hash.Write([]byte{0}) + } + }() + } + <-neverdie +} diff --git a/misc/windows-cpupercent/windows.dockerfile b/misc/windows-cpupercent/windows.dockerfile new file mode 100644 index 00000000000..482f47763eb --- /dev/null +++ b/misc/windows-cpupercent/windows.dockerfile @@ -0,0 +1,20 @@ +# Copyright 2017 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. +FROM golang:nanoserver + +WORKDIR /gopath +COPY main.go . + +RUN go build -o cpuhog main.go +ENTRYPOINT ["./cpuhog"] +CMD [ "-concurrency", "1000" ] diff --git a/misc/windows-deploy/Install-ECSAgent.ps1 b/misc/windows-deploy/Install-ECSAgent.ps1 new file mode 100644 index 00000000000..7f8d36c302b --- /dev/null +++ b/misc/windows-deploy/Install-ECSAgent.ps1 @@ -0,0 +1,139 @@ +# Copyright 2017 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. +[cmdletbinding()] +Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Path = "$($PSScriptRoot)", + + [Parameter(Mandatory=$false)] + [ValidateSet("Manual","AutomaticDelayedStart","Disabled")] + [string]$StartupType = 'Manual' +) +Begin { + $Script:ServiceName = "AmazonECS" + if (Get-Service | ?{$_.Name -eq "$($Script:ServiceName)"}) { + Write-Host "The $($Script:ServiceName) service already exists." + return + } + Function Initialize-Directory { + Param ( + [string]$Path, + [string]$Name + ) + [string]$targetDir = Join-Path "$($Path)" "$($Name)" + if (-Not (Test-Path $targetDir -ErrorAction:Ignore)) { + Write-Verbose "creating directory: $($targetDir)" + New-Item -Path $($targetDir) -ItemType:Directory -Force -ErrorAction:Continue | Out-Null + } else { + Write-Verbose "directory found: $($targetDir)" + } + return $targetDir + } + + # + # Setup Default Directories + # + Write-Verbose "Setting up default directories" + # C:\ProgramData\Amazon + [string]$Script:AmazonProgramData = Initialize-Directory -Path $ENV:ProgramData -Name "Amazon" + # C:\ProgramData\Amazon\ECS + [string]$Script:ECSProgramData = Initialize-Directory -Path $Script:AmazonProgramData -Name "ECS" + # C:\ProgramData\Amazon\ECS\data + [string]$Script:ECSData = Initialize-Directory -Path $Script:ECSProgramData -Name "data" + # C:\ProgramData\Amazon\ECS\log + [string]$Script:ECSLogs = Initialize-Directory -Path $Script:ECSProgramData -Name "log" + + # C:\Program Files\Amazon + [string]$Script:AmazonProgramFiles = Initialize-Directory -Path $ENV:ProgramFiles -Name "Amazon" + # C:\Program Files\Amazon\ECS + [string]$Script:ECSProgramFiles = Initialize-Directory -Path $Script:AmazonProgramFiles -Name "ECS" + + if (-not (Test-Path $Path)) { + Throw "The destination path provided does not exist: $($Path)" + return + } +} +Process { + # + # Service Config CONSTANTS + # + [int]$SERVICE_FAILURE_RESTART_DELAY_RESET_SEC = 300 + [int]$SERVICE_FAILURE_FIRST_DELAY_MS = 5000 + [int]$SERVICE_FAILURE_SECOND_DELAY_MS = 30000 + [int]$SERVICE_FAILURE_DELAY_MS = 60000 + + # + # Check for agent + # + $ECS_EXE = Join-Path (Get-Item $Path).FullName 'amazon-ecs-agent.exe' + if(-not (Test-Path $ECS_EXE)) { + Throw "Failed to find agent `"$($ECS_EXE)`"" + return + } + + # + # Setup Default Agent Settings + # + try { + [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "$($Script:ECSLogs)\ecs-agent.log", "Machine") + [Environment]::SetEnvironmentVariable("ECS_DATADIR", "$($Script:ECSData)", "Machine") + } catch { + Write-Host "Failed to set agent default configuration." + Throw $_ + } + + # + # Create Service + # + try { + $ServiceParams = @{ + Name = "$($Script:ServiceName)"; + BinaryPathName = "$($ECS_EXE) -windows-service"; + DisplayName = "Amazon ECS"; + Description = "The $($Script:ServiceName) service runs the Amazon ECS agent"; + DependsOn = 'Docker'; + } + if ($StartupType -eq 'AutomaticDelayedStart') { + $ServiceParams += @{ + StartupType = 'Automatic'; + } + } else { + $ServiceParams += @{ + StartupType = $StartupType; + } + } + Write-Verbose "Creating Service:" + Write-Verbose "$($ServiceParams | Out-String)" + $newService = New-Service @ServiceParams + if ($StartupType -eq 'AutomaticDelayedStart') { + Write-Verbose "Setting 'Automatic' StartupType to 'Delayed-Auto'" + $config = sc.exe config $($Script:ServiceName) Start= Delayed-Auto + Write-Verbose $config + } + Write-Verbose "Setting Service Restart Configuration:" + Write-Verbose " reset=300" + Write-Verbose $(" actions=restart/$($SERVICE_FAILURE_FIRST_DELAY_MS)/" + ` + "restart/$($SERVICE_FAILURE_SECOND_DELAY_MS)/" + ` + "restart/$($SERVICE_FAILURE_DELAY_MS)") + $failure = sc.exe failure $($Script:ServiceName) reset=300 actions=restart/$($SERVICE_FAILURE_FIRST_DELAY_MS)/restart/$($SERVICE_FAILURE_SECOND_DELAY_MS)/restart/$($SERVICE_FAILURE_DELAY_MS) + Write-Verbose $failure + $failureflag = sc.exe failureflag $($Script:ServiceName) 1 + Write-Verbose $failureflag + } catch { + Write-Host "Failed to create windows service for ECS agent" + Throw $_ + return + } + return $newService +} End { } \ No newline at end of file diff --git a/misc/windows-deploy/hostsetup.ps1 b/misc/windows-deploy/hostsetup.ps1 index 9fddd3eb8c1..8960dad3e12 100644 --- a/misc/windows-deploy/hostsetup.ps1 +++ b/misc/windows-deploy/hostsetup.ps1 @@ -19,6 +19,7 @@ $ErrorActionPreference = 'Continue' # 169.254.170.2:51679 is the IP address used for task IAM roles. $credentialAddress = "169.254.170.2" $credentialPort = "51679" +$loopbackAddress = "127.0.0.1" $adapter = (Get-NetAdapter -Name "*APIPA*") if(!($adapter)) { @@ -45,7 +46,7 @@ if(!($ip)) { # This forwards traffic from port 80 and listens on the IAM role IP address. # 'portproxy' doesn't have a powershell module equivalent, but we could move if it becomes available. - netsh interface portproxy add v4tov4 listenaddress=$credentialAddress listenport=80 connectaddress=$credentialAddress connectport=$credentialPort + netsh interface portproxy add v4tov4 listenaddress=$credentialAddress listenport=80 connectaddress=$loopbackAddress connectport=$credentialPort } $ErrorActionPreference=$oldActionPref diff --git a/misc/windows-python/build.ps1 b/misc/windows-python/build.ps1 new file mode 100644 index 00000000000..d9e9156c423 --- /dev/null +++ b/misc/windows-python/build.ps1 @@ -0,0 +1,15 @@ +# Copyright 2017 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. + +docker pull python:3-windowsservercore +docker tag python:3-windowsservercore amazon/amazon-ecs-windows-python:make diff --git a/scripts/run-functional-tests.ps1 b/scripts/run-functional-tests.ps1 index 7b78e046d09..045ac0026e2 100644 --- a/scripts/run-functional-tests.ps1 +++ b/scripts/run-functional-tests.ps1 @@ -13,6 +13,9 @@ Invoke-Expression "${PSScriptRoot}\..\misc\windows-iam\Setup_Iam.ps1" Invoke-Expression "${PSScriptRoot}\..\misc\windows-listen80\Setup_Listen80.ps1" +Invoke-Expression "${PSScriptRoot}\..\misc\windows-cpupercent\build.ps1" +Invoke-Expression "${PSScriptRoot}\..\misc\windows-python\build.ps1" + # Run the tests $cwd = (pwd).Path