From b8df8a5f0db55854107f8554d329312927ea10ed Mon Sep 17 00:00:00 2001 From: Harsh Rawat <65640262+harsh-rawat-a2z@users.noreply.github.com> Date: Mon, 6 Jul 2020 16:42:06 -0500 Subject: [PATCH] Awsvpc windows : Check if the pause image is loaded on the container instance (#2498) * Changes to check if the pause image has been loaded on agent startup. We will cache pause image on ECS-Optimized Windows AMI. On agent startup, we check if the pause image is already loaded. The following changes are made as part of this commit: 1. Moved common functions from pause_linux.go to load.go 2. Moved the common code from IsLoaded function to a function in load.go 3. Unit tests for the same were moved from Linux specific files to common files --- README.md | 2 +- agent/app/agent_windows.go | 6 +- agent/config/config_windows.go | 2 + agent/eni/pause/load.go | 33 ++++++ agent/eni/pause/load_test.go | 148 +++++++++++++++++++++++++++ agent/eni/pause/pause_linux.go | 28 +---- agent/eni/pause/pause_linux_test.go | 118 --------------------- agent/eni/pause/pause_unsupported.go | 2 +- agent/eni/pause/pause_windows.go | 36 +++++++ scripts/build | 18 ++-- scripts/build_agent.ps1 | 23 +++++ 11 files changed, 260 insertions(+), 156 deletions(-) create mode 100644 agent/eni/pause/load_test.go create mode 100644 agent/eni/pause/pause_windows.go create mode 100644 scripts/build_agent.ps1 diff --git a/README.md b/README.md index 461ea1f963d..da9c6e7b76e 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ The following targets are available. Each may be run with `make `. ### Standalone (on Windows) -The Amazon ECS Container Agent may be built by typing `go build -o amazon-ecs-agent.exe ./agent`. +The Amazon ECS Container Agent may be built by invoking `scripts\build_agent.ps1` ### Scripts (on Windows) diff --git a/agent/app/agent_windows.go b/agent/app/agent_windows.go index 5eb316926e6..85b22da953b 100644 --- a/agent/app/agent_windows.go +++ b/agent/app/agent_windows.go @@ -283,5 +283,9 @@ func (agent *ecsAgent) getPlatformDevices() []*ecs.PlatformDevice { } func (agent *ecsAgent) loadPauseContainer() error { - return nil + // The pause image would be cached in th ECS-Optimized Windows AMI's and will be available. We will throw an error if the image is not loaded. + // If the agent is run on non-supported instances then pause image has to be loaded manually by the client. + _, err := agent.pauseLoader.IsLoaded(agent.dockerClient) + + return err } diff --git a/agent/config/config_windows.go b/agent/config/config_windows.go index a94b5558a27..c909d29085f 100644 --- a/agent/config/config_windows.go +++ b/agent/config/config_windows.go @@ -121,6 +121,8 @@ func DefaultConfig() Config { PollingMetricsWaitDuration: DefaultPollingMetricsWaitDuration, GMSACapable: true, FSxWindowsFileServerCapable: true, + PauseContainerImageName: DefaultPauseContainerImageName, + PauseContainerTag: DefaultPauseContainerTag, } } diff --git a/agent/eni/pause/load.go b/agent/eni/pause/load.go index ec5a2f76810..a20d2e9ea22 100644 --- a/agent/eni/pause/load.go +++ b/agent/eni/pause/load.go @@ -15,10 +15,13 @@ package pause import ( "context" + "fmt" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + log "github.com/cihub/seelog" "github.com/docker/docker/api/types" + "github.com/pkg/errors" ) // Loader defines an interface for loading the pause container image. This is mostly @@ -34,3 +37,33 @@ type loader struct{} func New() Loader { return &loader{} } + +// This function uses the DockerClient to inspect the image with the given name and tag. +func getPauseContainerImage(name string, tag string, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + imageName := fmt.Sprintf("%s:%s", name, tag) + log.Debugf("Inspecting pause container image: %s", imageName) + + image, err := dockerClient.InspectImage(imageName) + if err != nil { + return nil, errors.Wrapf(err, + "pause container load: failed to inspect image: %s", imageName) + } + + return image, nil +} + +// Common function for linux and windows to check if the container pause image has been loaded +func isImageLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + image, err := getPauseContainerImage( + config.DefaultPauseContainerImageName, config.DefaultPauseContainerTag, dockerClient) + + if err != nil { + return false, err + } + + if image == nil || image.ID == "" { + return false, nil + } + + return true, nil +} diff --git a/agent/eni/pause/load_test.go b/agent/eni/pause/load_test.go new file mode 100644 index 00000000000..48b8dcb254f --- /dev/null +++ b/agent/eni/pause/load_test.go @@ -0,0 +1,148 @@ +// +build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package pause + +import ( + "context" + "errors" + "testing" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks" + mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks" + + "github.com/docker/docker/api/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +const ( + pauseName = "pause" + pauseTag = "tag" +) + +var defaultConfig = config.DefaultConfig() + +func TestGetPauseContainerImageInspectImageError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return( + types.ImageInspect{}, nil, errors.New("error")) + + _, err = getPauseContainerImage(pauseName, pauseTag, client) + assert.Error(t, err) +} + +func TestGetPauseContainerHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return(types.ImageInspect{}, nil, nil) + + _, err = getPauseContainerImage(pauseName, pauseTag, client) + assert.NoError(t, err) +} + +func TestIsImageLoadedHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{ID: "test123"}, nil, nil) + + isLoaded, err := isImageLoaded(client) + assert.NoError(t, err) + assert.True(t, isLoaded) +} + +func TestIsImageLoadedNotLoaded(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{}, nil, nil) + + isLoaded, err := isImageLoaded(client) + assert.NoError(t, err) + assert.False(t, isLoaded) +} + +func TestIsImageLoadedError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return( + types.ImageInspect{}, nil, errors.New("error")) + + isLoaded, err := isImageLoaded(client) + assert.Error(t, err) + assert.False(t, isLoaded) +} diff --git a/agent/eni/pause/pause_linux.go b/agent/eni/pause/pause_linux.go index a89c2f7b023..6abbda40715 100644 --- a/agent/eni/pause/pause_linux.go +++ b/agent/eni/pause/pause_linux.go @@ -17,7 +17,6 @@ package pause import ( "context" - "fmt" "os" "github.com/aws/amazon-ecs-agent/agent/config" @@ -41,19 +40,7 @@ func (*loader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient d } func (*loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - image, err := getPauseContainerImage( - config.DefaultPauseContainerImageName, config.DefaultPauseContainerTag, dockerClient) - - if err != nil { - return false, errors.Wrapf(err, - "pause container inspect: failed to inspect image: %s", config.DefaultPauseContainerImageName) - } - - if image == nil || image.ID == "" { - return false, nil - } - - return true, nil + return isImageLoaded(dockerClient) } var open = os.Open @@ -76,16 +63,3 @@ func loadFromFile(ctx context.Context, path string, dockerClient dockerapi.Docke return nil } - -func getPauseContainerImage(name string, tag string, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - imageName := fmt.Sprintf("%s:%s", name, tag) - log.Debugf("Inspecting pause container image: %s", imageName) - - image, err := dockerClient.InspectImage(imageName) - if err != nil { - return nil, errors.Wrapf(err, - "pause container load: failed to inspect image: %s", imageName) - } - - return image, nil -} diff --git a/agent/eni/pause/pause_linux_test.go b/agent/eni/pause/pause_linux_test.go index 48894db4ab1..316c5c0ced6 100644 --- a/agent/eni/pause/pause_linux_test.go +++ b/agent/eni/pause/pause_linux_test.go @@ -21,7 +21,6 @@ import ( "os" "testing" - "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks" mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks" @@ -33,12 +32,8 @@ import ( const ( pauseTarballPath = "/path/to/pause.tar" - pauseName = "pause" - pauseTag = "tag" ) -var defaultConfig = config.DefaultConfig() - func mockOpen() func() { open = func(name string) (*os.File, error) { return nil, nil @@ -124,116 +119,3 @@ func TestLoadFromFileDockerLoadImageError(t *testing.T) { err = loadFromFile(ctx, pauseTarballPath, client) assert.Error(t, err) } - -func TestGetPauseContainerImageInspectImageError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return( - types.ImageInspect{}, nil, errors.New("error")) - - _, err = getPauseContainerImage(pauseName, pauseTag, client) - assert.Error(t, err) -} - -func TestGetPauseContainerHappyPath(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return(types.ImageInspect{}, nil, nil) - - _, err = getPauseContainerImage(pauseName, pauseTag, client) - assert.NoError(t, err) -} - -func TestIsLoadedHappyPath(t *testing.T) { - ctrl := gomock.NewController(t) - pauseLoader := New() - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{ID: "test123"}, nil, nil) - - isLoaded, err := pauseLoader.IsLoaded(client) - assert.NoError(t, err) - assert.True(t, isLoaded) -} - -func TestIsLoadedNotLoaded(t *testing.T) { - ctrl := gomock.NewController(t) - pauseLoader := New() - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{}, nil, nil) - - isLoaded, err := pauseLoader.IsLoaded(client) - assert.NoError(t, err) - assert.False(t, isLoaded) -} - -func TestIsLoadedError(t *testing.T) { - ctrl := gomock.NewController(t) - pauseLoader := New() - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return( - types.ImageInspect{}, nil, errors.New("error")) - - isLoaded, err := pauseLoader.IsLoaded(client) - assert.Error(t, err) - assert.False(t, isLoaded) -} diff --git a/agent/eni/pause/pause_unsupported.go b/agent/eni/pause/pause_unsupported.go index 80727a56a86..f7a50f86dee 100644 --- a/agent/eni/pause/pause_unsupported.go +++ b/agent/eni/pause/pause_unsupported.go @@ -1,4 +1,4 @@ -// +build !linux +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/pause/pause_windows.go b/agent/eni/pause/pause_windows.go new file mode 100644 index 00000000000..eced45ee43c --- /dev/null +++ b/agent/eni/pause/pause_windows.go @@ -0,0 +1,36 @@ +// +build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package pause + +import ( + "context" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/docker/docker/api/types" + "github.com/pkg/errors" +) + +// In Linux, we use a tar archive to load the pause image. Whereas in Windows, we will cache the image during AMI build. +// Therefore, this functionality is not supported in Windows. +func (*loader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + return nil, errors.New("this functionality is not supported on this platform.") +} + +// This method is used to inspect the presence of the pause image. If the image has not been loaded then we return false. +func (*loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return isImageLoaded(dockerClient) +} diff --git a/scripts/build b/scripts/build index 05c63ffac1d..5a1defc5d75 100755 --- a/scripts/build +++ b/scripts/build @@ -31,10 +31,10 @@ ROOT=$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd ) cd "${ROOT}" if [[ "${version_gen}" == "true" ]]; then - # Versioning stuff. We run the generator to setup the version and then always - # restore ourselves to a clean state - cp agent/version/version.go agent/version/_version.go - trap "cd \"${ROOT}\"; mv agent/version/_version.go agent/version/version.go" EXIT SIGHUP SIGINT SIGTERM + # Versioning stuff. We run the generator to setup the version and then always + # restore ourselves to a clean state + cp agent/version/version.go agent/version/_version.go + trap "cd \"${ROOT}\"; mv agent/version/_version.go agent/version/version.go" EXIT SIGHUP SIGINT SIGTERM cd ./agent/version/ # Turn off go module here because version-gen.go is a separate program (i.e. "package main") @@ -52,12 +52,14 @@ else fi cd "${ROOT}" -if [[ "${static}" == "true" ]]; then - CGO_ENABLED=0 go build -installsuffix cgo -a -ldflags "${LDFLAGS} -s" -o $build_exe ./agent/ +if [[ "${TARGET_OS}" == "windows" ]]; then + go build -ldflags "${LDFLAGS} -s" -o $build_exe ./agent/ +elif [[ "${static}" == "true" ]]; then + CGO_ENABLED=0 go build -installsuffix cgo -a -ldflags "${LDFLAGS} -s" -o $build_exe ./agent/ else - go build -o $build_exe ./agent/ + go build -o $build_exe ./agent/ fi if [[ -n "${output_directory}" ]]; then - mv $build_exe "${output_directory}" + mv $build_exe "${output_directory}" fi diff --git a/scripts/build_agent.ps1 b/scripts/build_agent.ps1 new file mode 100644 index 00000000000..d1681fd1618 --- /dev/null +++ b/scripts/build_agent.ps1 @@ -0,0 +1,23 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +# Standalone Amazon ECS Container Agent for Windows may be built by using this script + +$pauseImageName = "amazon/amazon-ecs-pause" +$pauseImageTag = "0.1.0" + +$build_exe = "../amazon-ecs-agent.exe" +$ldflag = "-X github.com/aws/amazon-ecs-agent/agent/config.DefaultPauseContainerTag=$pauseImageTag -X github.com/aws/amazon-ecs-agent/agent/config.DefaultPauseContainerImageName=$pauseImageName" + + +go build -ldflags "$ldflag" -o $build_exe ../agent/ \ No newline at end of file