From 2984b06410f5c13345b43aed8a6ece3c1bd5c801 Mon Sep 17 00:00:00 2001 From: Saurabh Chakravarty Date: Mon, 3 Jun 2024 22:38:19 +0000 Subject: [PATCH] Added the implementation for DaemonManager and ManagedDaemon for Windows. --- agent/api/task/task.go | 9 +- agent/api/task/task_linux.go | 9 + agent/api/task/task_windows.go | 6 + agent/ebs/watcher.go | 3 +- agent/ebs/watcher_linux.go | 22 ++ agent/ebs/watcher_windows.go | 22 ++ agent/engine/daemonmanager/daemon_manager.go | 181 ++++++++++++ .../daemonmanager/daemon_manager_linux.go | 176 ------------ .../daemon_manager_linux_test.go | 253 ---------------- .../daemonmanager/daemon_manager_test.go | 269 ++++++++++++++++++ .../daemonmanager/daemon_manager_windows.go | 59 +--- .../daemon_manager_windows_test.go | 28 ++ agent/utils/utils_windows.go | 17 ++ .../ecs-agent/csiclient/csi_client.go | 8 +- .../ecs-agent/csiclient/csi_client_linux.go | 6 - .../ecs-agent/csiclient/csi_client_windows.go | 12 +- .../ecs-agent/manageddaemon/managed_daemon.go | 76 +---- .../manageddaemon/managed_daemon_linux.go | 98 +++++++ .../manageddaemon/managed_daemon_windows.go | 106 +++++++ ecs-agent/csiclient/csi_client.go | 8 +- ecs-agent/csiclient/csi_client_linux.go | 6 - ecs-agent/csiclient/csi_client_windows.go | 12 +- ecs-agent/manageddaemon/managed_daemon.go | 9 +- .../manageddaemon/managed_daemon_linux.go | 2 +- .../managed_daemon_linux_test.go | 27 ++ .../manageddaemon/managed_daemon_test.go | 12 +- .../manageddaemon/managed_daemon_windows.go | 89 +++--- .../managed_daemon_windows_test.go | 27 ++ 28 files changed, 919 insertions(+), 633 deletions(-) create mode 100644 agent/ebs/watcher_linux.go create mode 100644 agent/ebs/watcher_windows.go create mode 100644 agent/engine/daemonmanager/daemon_manager_test.go create mode 100644 agent/engine/daemonmanager/daemon_manager_windows_test.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_linux.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_windows.go create mode 100644 ecs-agent/manageddaemon/managed_daemon_linux_test.go create mode 100644 ecs-agent/manageddaemon/managed_daemon_windows_test.go diff --git a/agent/api/task/task.go b/agent/api/task/task.go index c7f5b96b518..df85e48e5b3 100644 --- a/agent/api/task/task.go +++ b/agent/api/task/task.go @@ -1747,17 +1747,14 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc Env: dockerEnv, } - // TODO [SC] - Move this as well as 'dockerExposedPorts' SC-specific logic into a separate file - if (task.IsServiceConnectEnabled() && container == task.GetServiceConnectContainer()) || - container.Type == apicontainer.ContainerServiceConnectRelay { - containerConfig.User = strconv.Itoa(serviceconnect.AppNetUID) - } - if container.DockerConfig.Config != nil { if err := json.Unmarshal([]byte(aws.StringValue(container.DockerConfig.Config)), &containerConfig); err != nil { return nil, &apierrors.DockerClientConfigError{Msg: "Unable decode given docker config: " + err.Error()} } } + + containerConfig.User = task.getTaskUser(container) + if container.HealthCheckType == apicontainer.DockerHealthCheckType && containerConfig.Healthcheck == nil { return nil, &apierrors.DockerClientConfigError{ Msg: "docker health check is nil while container health check type is DOCKER"} diff --git a/agent/api/task/task_linux.go b/agent/api/task/task_linux.go index 79dcbb13c31..6ffe670161d 100644 --- a/agent/api/task/task_linux.go +++ b/agent/api/task/task_linux.go @@ -265,6 +265,15 @@ func (task *Task) initializeFSxWindowsFileServerResource(cfg *config.Config, cre return errors.New("task with FSx for Windows File Server volumes is only supported on Windows container instance") } +func (task *Task) getTaskUser(container *container.Container) string { + var user string + if (task.IsServiceConnectEnabled() && container == task.GetServiceConnectContainer()) || + container.Type == apicontainer.ContainerServiceConnectRelay { + user = strconv.Itoa(serviceconnect.AppNetUID) + } + return user +} + // BuildCNIConfigAwsvpc builds a list of CNI network configurations for the task. // If includeIPAMConfig is set to true, the list also includes the bridge IPAM configuration. func (task *Task) BuildCNIConfigAwsvpc(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { diff --git a/agent/api/task/task_windows.go b/agent/api/task/task_windows.go index 832785089a0..65d76e81325 100644 --- a/agent/api/task/task_windows.go +++ b/agent/api/task/task_windows.go @@ -19,6 +19,7 @@ package task import ( "time" + "github.com/aws/amazon-ecs-agent/agent/api/container" "github.com/aws/amazon-ecs-agent/agent/ecscni" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" @@ -214,6 +215,11 @@ func (task *Task) addFSxWindowsFileServerResource( return nil } +// For Windows task run via the docker DaemonManager, we will run them as ContainerAdministrator +func (task *Task) getTaskUser(container *container.Container) string { + return "ContainerAdministrator" +} + // BuildCNIConfigAwsvpc builds a list of CNI network configurations for the task. func (task *Task) BuildCNIConfigAwsvpc(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { if !task.IsNetworkModeAWSVPC() { diff --git a/agent/ebs/watcher.go b/agent/ebs/watcher.go index 5fe069910ee..0ad04ff5c70 100644 --- a/agent/ebs/watcher.go +++ b/agent/ebs/watcher.go @@ -38,7 +38,6 @@ import ( const ( nodeStageTimeout = 2 * time.Second - hostMountDir = "/mnt/ecs/ebs" ) type EBSWatcher struct { @@ -63,7 +62,7 @@ func NewWatcher(ctx context.Context, derivedContext, cancel := context.WithCancel(ctx) discoveryClient := apiebs.NewDiscoveryClient(derivedContext) // TODO pull this socket out into config - csiClient := csi.NewCSIClient("/var/run/ecs/ebs-csi-driver/csi-driver.sock") + csiClient := csi.NewCSIClient(csiDriverSocketAddress) return &EBSWatcher{ ctx: derivedContext, cancel: cancel, diff --git a/agent/ebs/watcher_linux.go b/agent/ebs/watcher_linux.go new file mode 100644 index 00000000000..99a252f7009 --- /dev/null +++ b/agent/ebs/watcher_linux.go @@ -0,0 +1,22 @@ +//go:build linux +// +build linux + +// 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 ebs + +const ( + csiDriverSocketAddress = "/var/run/ecs/ebs-csi-driver/csi-driver.sock" + hostMountDir = "/mnt/ecs/ebs" +) diff --git a/agent/ebs/watcher_windows.go b/agent/ebs/watcher_windows.go new file mode 100644 index 00000000000..f4deffaabf1 --- /dev/null +++ b/agent/ebs/watcher_windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package ebs + +const ( + csiDriverSocketAddress = "C:\\ProgramData\\Amazon\\ECS\\ebs-csi-driver\\csi-driver.sock" + hostMountDir = "C:\\var\\lib\\kubelet" +) diff --git a/agent/engine/daemonmanager/daemon_manager.go b/agent/engine/daemonmanager/daemon_manager.go index ad3ffaf4961..2d75d51d76d 100644 --- a/agent/engine/daemonmanager/daemon_manager.go +++ b/agent/engine/daemonmanager/daemon_manager.go @@ -15,11 +15,27 @@ package daemonmanager import ( "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/taskresource" + "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" + apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status" + apitaskstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" + "github.com/aws/aws-sdk-go/aws" "github.com/docker/docker/api/types" + dockercontainer "github.com/docker/docker/api/types/container" + dockermount "github.com/docker/docker/api/types/mount" + "github.com/pborman/uuid" ) type DaemonManager interface { @@ -44,3 +60,168 @@ func NewDaemonManager(manageddaemon *md.ManagedDaemon) DaemonManager { func (dm *daemonManager) GetManagedDaemon() *md.ManagedDaemon { return dm.managedDaemon } + +var mkdirAllAndChown = utils.MkdirAllAndChown + +func (dm *daemonManager) CreateDaemonTask() (*apitask.Task, error) { + imageName := dm.managedDaemon.GetImageName() + // create daemon-specific host mounts + if err := dm.initDaemonDirectoryMounts(imageName); err != nil { + logger.Error("initDaemonDirectory failure", + logger.Fields{ + field.Error: err, + }) + return nil, err + } + + loadedImageRef := dm.managedDaemon.GetLoadedDaemonImageRef() + containerRunning := apicontainerstatus.ContainerRunning + stringCaps := []string{} + if dm.managedDaemon.GetLinuxParameters() != nil { + caps := dm.managedDaemon.GetLinuxParameters().Capabilities.Add + for _, cap := range caps { + stringCaps = append(stringCaps, *cap) + } + } + dockerHostConfig := dockercontainer.HostConfig{ + Mounts: []dockermount.Mount{}, + NetworkMode: apitask.HostNetworkMode, + // the default value of 0 for MaximumRetryCount means retry indefinitely + RestartPolicy: dockercontainer.RestartPolicy{ + Name: "on-failure", + MaximumRetryCount: 0, + }, + Privileged: dm.managedDaemon.GetPrivileged(), + CapAdd: stringCaps, + } + if !dm.managedDaemon.IsValidManagedDaemon() { + return nil, fmt.Errorf("%s is an invalid managed daemon", imageName) + } + + for _, mp := range dm.managedDaemon.GetMountPoints() { + if err := mkdirAllAndChown(mp.SourceVolumeHostPath, daemonMountPermission, daemonUID, os.Getegid()); err != nil { + return nil, err + } + var bindOptions = dockermount.BindOptions{} + + if mp.PropagationShared { + // https://github.com/moby/moby/blob/master/api/types/mount/mount.go#L52 + bindOptions = dockermount.BindOptions{Propagation: dockermount.PropagationShared} + } + logger.Info(fmt.Sprintf("bindMount Propagation: %s", bindOptions.Propagation), + logger.Fields{ + field.Image: loadedImageRef, + }) + typeBind := dockermount.TypeBind + mountPoint := dockermount.Mount{} + if mp.SourceVolumeType == "npipe" { + typeBind = dockermount.TypeNamedPipe + mountPoint = dockermount.Mount{ + Type: typeBind, + Source: mp.SourceVolumeHostPath, + Target: mp.ContainerPath, + } + } else { + mountPoint = dockermount.Mount{ + Type: typeBind, + Source: mp.SourceVolumeHostPath, + Target: mp.ContainerPath, + BindOptions: &bindOptions, + } + } + dockerHostConfig.Mounts = append(dockerHostConfig.Mounts, mountPoint) + } + rawHostConfig, err := json.Marshal(&dockerHostConfig) + if err != nil { + return nil, err + } + healthConfig := dm.managedDaemon.GetDockerHealthConfig() + var rawConfig = "" + rawHealthConfig, err := json.Marshal(&healthConfig) + if err != nil { + return nil, err + } + // The raw host config needs to be created this way - if we marshal the entire config object + // directly, and the object only contains healthcheck, all other fields will be written as empty/nil + // in the result string. This will override the configurations that comes with the container image + // (CMD for example) + // TODO update User in raw config to use either runAs user or runAsRoot from managed daemon config + rawConfig = fmt.Sprintf("{\"Healthcheck\":%s, \"User\":\"0\"}", string(rawHealthConfig)) + daemonTask := &apitask.Task{ + Arn: fmt.Sprintf("arn:::::/%s-%s", dm.managedDaemon.GetImageName(), uuid.NewUUID()), + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + Containers: []*apicontainer.Container{{ + // name must be unique among running containers + // We should only have a single managed daemon of each type per instance + Name: fmt.Sprintf("ecs-managed-%s", dm.managedDaemon.GetImageName()), + Image: loadedImageRef, + ContainerArn: fmt.Sprintf("arn:::::/instance-%s", imageName), + Type: apicontainer.ContainerManagedDaemon, + Command: dm.managedDaemon.GetCommand(), + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + Essential: true, + SteadyStateStatusUnsafe: &containerRunning, + DockerConfig: apicontainer.DockerConfig{ + Config: aws.String(rawConfig), + HostConfig: aws.String(string(rawHostConfig)), + }, + HealthCheckType: "DOCKER", + }}, + LaunchType: "EC2", + NetworkMode: apitask.HostNetworkMode, + ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + IsInternal: true, + } + // add managed daemon environment to daemon task container + daemonTask.Containers[0].MergeEnvironmentVariables(dm.managedDaemon.GetEnvironment()) + return daemonTask, nil +} + +func (dm *daemonManager) initDaemonDirectoryMounts(imageName string) error { + // create logging directory + logPathHost := filepath.Join(logPathHostRoot, imageName) + if err := mkdirAllAndChown(logPathHost, daemonLogPermission, daemonUID, os.Getegid()); err != nil { + return err + } + // create socket path + socketPathHost := filepath.Join(socketPathHostRoot, imageName) + if err := mkdirAllAndChown(socketPathHost, daemonMountPermission, daemonUID, os.Getegid()); err != nil { + return err + } + return nil +} + +// Returns true if the Daemon image is found on this host, false otherwise. +func (dm *daemonManager) ImageExists() (bool, error) { + return utils.FileExists(dm.managedDaemon.GetImageTarPath()) +} + +// LoadImage loads the daemon's latest image +func (dm *daemonManager) LoadImage(ctx context.Context, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + var loadErr error + daemonImageToLoad := dm.managedDaemon.GetImageName() + daemonImageTarPath := dm.managedDaemon.GetImageTarPath() + if _, err := os.Stat(daemonImageTarPath); err != nil { + logger.Warn(fmt.Sprintf("%s container tarball unavailable at path: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ + field.Error: err, + }) + } + logger.Debug(fmt.Sprintf("Loading %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath)) + if loadErr = loader.LoadFromFile(ctx, daemonImageTarPath, dockerClient); loadErr != nil { + logger.Warn(fmt.Sprintf("Unable to load %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ + field.Error: loadErr, + }) + } + dm.managedDaemon.SetLoadedDaemonImageRef(dm.managedDaemon.GetImageRef()) + loadedImageRef := dm.managedDaemon.GetLoadedDaemonImageRef() + logger.Info(fmt.Sprintf("Successfully loaded %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), + logger.Fields{ + field.Image: loadedImageRef, + }) + return loader.GetContainerImage(loadedImageRef, dockerClient) +} + +// isImageLoaded uses the image ref with its tag +func (dm *daemonManager) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return loader.IsImageLoaded(dm.managedDaemon.GetImageRef(), dockerClient) +} diff --git a/agent/engine/daemonmanager/daemon_manager_linux.go b/agent/engine/daemonmanager/daemon_manager_linux.go index 86df0277ba3..2be3ee4b820 100644 --- a/agent/engine/daemonmanager/daemon_manager_linux.go +++ b/agent/engine/daemonmanager/daemon_manager_linux.go @@ -17,29 +17,7 @@ package daemonmanager import ( - "context" - "encoding/json" - "fmt" "io/fs" - "os" - "path/filepath" - - apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" - apitask "github.com/aws/amazon-ecs-agent/agent/api/task" - "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" - "github.com/aws/amazon-ecs-agent/agent/taskresource" - utils "github.com/aws/amazon-ecs-agent/agent/utils" - "github.com/aws/amazon-ecs-agent/agent/utils/loader" - apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status" - apitaskstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status" - "github.com/aws/amazon-ecs-agent/ecs-agent/logger" - "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" - - "github.com/aws/aws-sdk-go/aws" - "github.com/docker/docker/api/types" - dockercontainer "github.com/docker/docker/api/types/container" - dockermount "github.com/docker/docker/api/types/mount" - "github.com/pborman/uuid" ) const ( @@ -51,157 +29,3 @@ const ( socketPathHostRoot = "/var/run/ecs" logPathHostRoot = "/log/daemons" ) - -var mkdirAllAndChown = utils.MkdirAllAndChown - -func (dm *daemonManager) CreateDaemonTask() (*apitask.Task, error) { - imageName := dm.managedDaemon.GetImageName() - // create daemon-specific host mounts - if err := dm.initDaemonDirectoryMounts(imageName); err != nil { - logger.Error("initDaemonDirectory failure", - logger.Fields{ - field.Error: err, - }) - return nil, err - } - - loadedImageRef := dm.managedDaemon.GetLoadedDaemonImageRef() - containerRunning := apicontainerstatus.ContainerRunning - stringCaps := []string{} - if dm.managedDaemon.GetLinuxParameters() != nil { - caps := dm.managedDaemon.GetLinuxParameters().Capabilities.Add - for _, cap := range caps { - stringCaps = append(stringCaps, *cap) - } - } - dockerHostConfig := dockercontainer.HostConfig{ - Mounts: []dockermount.Mount{}, - NetworkMode: apitask.HostNetworkMode, - // the default value of 0 for MaximumRetryCount means retry indefinitely - RestartPolicy: dockercontainer.RestartPolicy{ - Name: "on-failure", - MaximumRetryCount: 0, - }, - Privileged: dm.managedDaemon.GetPrivileged(), - CapAdd: stringCaps, - } - if !dm.managedDaemon.IsValidManagedDaemon() { - return nil, fmt.Errorf("%s is an invalid managed daemon", imageName) - } - - for _, mp := range dm.managedDaemon.GetMountPoints() { - if err := mkdirAllAndChown(mp.SourceVolumeHostPath, daemonMountPermission, daemonUID, os.Getegid()); err != nil { - return nil, err - } - var bindOptions = dockermount.BindOptions{} - - if mp.PropagationShared { - // https://github.com/moby/moby/blob/master/api/types/mount/mount.go#L52 - bindOptions = dockermount.BindOptions{Propagation: dockermount.PropagationShared} - } - logger.Info(fmt.Sprintf("bindMount Propagation: %s", bindOptions.Propagation), - logger.Fields{ - field.Image: loadedImageRef, - }) - mountPoint := dockermount.Mount{ - Type: dockermount.TypeBind, - Source: mp.SourceVolumeHostPath, - Target: mp.ContainerPath, - BindOptions: &bindOptions, - } - dockerHostConfig.Mounts = append(dockerHostConfig.Mounts, mountPoint) - } - rawHostConfig, err := json.Marshal(&dockerHostConfig) - if err != nil { - return nil, err - } - healthConfig := dm.managedDaemon.GetDockerHealthConfig() - var rawConfig = "" - rawHealthConfig, err := json.Marshal(&healthConfig) - if err != nil { - return nil, err - } - // The raw host config needs to be created this way - if we marshal the entire config object - // directly, and the object only contains healthcheck, all other fields will be written as empty/nil - // in the result string. This will override the configurations that comes with the container image - // (CMD for example) - // TODO update User in raw config to use either runAs user or runAsRoot from managed daemon config - rawConfig = fmt.Sprintf("{\"Healthcheck\":%s, \"User\":\"0\"}", string(rawHealthConfig)) - daemonTask := &apitask.Task{ - Arn: fmt.Sprintf("arn:::::/%s-%s", dm.managedDaemon.GetImageName(), uuid.NewUUID()), - DesiredStatusUnsafe: apitaskstatus.TaskRunning, - Containers: []*apicontainer.Container{{ - // name must be unique among running containers - // We should only have a single managed daemon of each type per instance - Name: fmt.Sprintf("ecs-managed-%s", dm.managedDaemon.GetImageName()), - Image: loadedImageRef, - ContainerArn: fmt.Sprintf("arn:::::/instance-%s", imageName), - Type: apicontainer.ContainerManagedDaemon, - Command: dm.managedDaemon.GetCommand(), - TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), - Essential: true, - SteadyStateStatusUnsafe: &containerRunning, - DockerConfig: apicontainer.DockerConfig{ - Config: aws.String(rawConfig), - HostConfig: aws.String(string(rawHostConfig)), - }, - HealthCheckType: "DOCKER", - }}, - LaunchType: "EC2", - NetworkMode: apitask.HostNetworkMode, - ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), - IsInternal: true, - } - // add managed daemon environment to daemon task container - daemonTask.Containers[0].MergeEnvironmentVariables(dm.managedDaemon.GetEnvironment()) - return daemonTask, nil -} - -func (dm *daemonManager) initDaemonDirectoryMounts(imageName string) error { - // create logging directory - logPathHost := filepath.Join(logPathHostRoot, imageName) - if err := mkdirAllAndChown(logPathHost, daemonLogPermission, daemonUID, os.Getegid()); err != nil { - return err - } - // create socket path - socketPathHost := filepath.Join(socketPathHostRoot, imageName) - if err := mkdirAllAndChown(socketPathHost, daemonMountPermission, daemonUID, os.Getegid()); err != nil { - return err - } - return nil -} - -// Returns true if the Daemon image is found on this host, false otherwise. -func (dm *daemonManager) ImageExists() (bool, error) { - return utils.FileExists(dm.managedDaemon.GetImageTarPath()) -} - -// LoadImage loads the daemon's latest image -func (dm *daemonManager) LoadImage(ctx context.Context, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - var loadErr error - daemonImageToLoad := dm.managedDaemon.GetImageName() - daemonImageTarPath := dm.managedDaemon.GetImageTarPath() - if _, err := os.Stat(daemonImageTarPath); err != nil { - logger.Warn(fmt.Sprintf("%s container tarball unavailable at path: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ - field.Error: err, - }) - } - logger.Debug(fmt.Sprintf("Loading %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath)) - if loadErr = loader.LoadFromFile(ctx, daemonImageTarPath, dockerClient); loadErr != nil { - logger.Warn(fmt.Sprintf("Unable to load %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ - field.Error: loadErr, - }) - } - dm.managedDaemon.SetLoadedDaemonImageRef(dm.managedDaemon.GetImageRef()) - loadedImageRef := dm.managedDaemon.GetLoadedDaemonImageRef() - logger.Info(fmt.Sprintf("Successfully loaded %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), - logger.Fields{ - field.Image: loadedImageRef, - }) - return loader.GetContainerImage(loadedImageRef, dockerClient) -} - -// isImageLoaded uses the image ref with its tag -func (dm *daemonManager) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - return loader.IsImageLoaded(dm.managedDaemon.GetImageRef(), dockerClient) -} diff --git a/agent/engine/daemonmanager/daemon_manager_linux_test.go b/agent/engine/daemonmanager/daemon_manager_linux_test.go index fb7d9a4ddd1..78f9ade91d6 100644 --- a/agent/engine/daemonmanager/daemon_manager_linux_test.go +++ b/agent/engine/daemonmanager/daemon_manager_linux_test.go @@ -16,21 +16,6 @@ package daemonmanager -import ( - "encoding/json" - "fmt" - "io/fs" - "strings" - "testing" - "time" - - apitask "github.com/aws/amazon-ecs-agent/agent/api/task" - apitaskstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status" - md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" - "github.com/aws/aws-sdk-go/aws" - "github.com/stretchr/testify/assert" -) - const ( TestDaemonName = "TestDaemon" TestImageRef = "TestImage" @@ -41,241 +26,3 @@ const ( TestAgentPath = "/test/agent/path" TestMountPointVolume = "testVolume" ) - -func TestCreateDaemonTask(t *testing.T) { - - cases := []struct { - testName string - testDaemonName string - testImageRef string - testOtherMount *md.MountPoint - testHealthCheck []string - }{ - { - testName: "Basic Daemon", - testDaemonName: TestDaemonName, - testImageRef: TestImageRef, - testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, - testHealthCheck: []string{TestHealthString}, - }, - { - testName: "Daemon Updated Daemon Name", - testDaemonName: "TestDeemen", - testImageRef: TestImageRef, - testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, - testHealthCheck: []string{TestHealthString}, - }, - { - testName: "Daemon Updated Image ref", - testDaemonName: TestDaemonName, - testImageRef: "TestOtherImage", - testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, - testHealthCheck: []string{TestHealthString}, - }, - { - testName: "Daemon With Updated Mounts", - testDaemonName: TestDaemonName, - testImageRef: TestImageRef, - testOtherMount: &md.MountPoint{SourceVolumeID: "testUpdatedMountVolume", ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, - testHealthCheck: []string{TestHealthString}, - }, - { - testName: "Daemon With Updated HealthCheck", - testDaemonName: TestDaemonName, - testImageRef: TestImageRef, - testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/"}, - testHealthCheck: []string{"testOtherHealth"}, - }, - } - - for _, c := range cases { - t.Run(c.testName, func(t *testing.T) { - // mock mkdirAllAndChown - origMkdir := mkdirAllAndChown - defer func() { mkdirAllAndChown = origMkdir }() - mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { - return nil - } - // set up test managed daemon - tmd := md.NewManagedDaemon(c.testDaemonName, TestImageTag) - tmd.SetLoadedDaemonImageRef(c.testImageRef) - // create required mounts and add all - testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} - testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} - testMountPoints := []*md.MountPoint{} - testMountPoints = append(testMountPoints, testAgentCommunicationMount, testApplicationLogMount, c.testOtherMount) - tmd.SetMountPoints(testMountPoints) - // add required healthcheck - tmd.SetHealthCheck(c.testHealthCheck, 2*time.Minute, 2*time.Minute, 1) - // create daemon manager for the above daemon - testDaemonManager := NewDaemonManager(tmd) - resultDaemonTask, _ := testDaemonManager.CreateDaemonTask() - // validate daemon task configs - assert.Equal(t, fmt.Sprintf("arn:::::/%s-", c.testDaemonName), resultDaemonTask.Arn[:20], "Task Arn prefix should match Image Name ") - assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") - assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") - assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") - assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInteral should be true") - // validate task container - assert.Equal(t, c.testImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") - // validate daemon container configs - configRaw := resultDaemonTask.Containers[0].DockerConfig.Config - hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig - var configMap map[string]interface{} - var hostConfigMap map[string]interface{} - json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) - json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) - // validate mount point count - if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { - assert.Equal(t, len(containerMounts), 3, "Task should have Required container binds (2) + 1 other bind") - } else { - t.Errorf("Unable to find 'Mounts' in container definition map") - } - // validate healthcheck - containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) - containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) - assert.Equal(t, c.testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") - }) - } -} - -func TestFailCreateDaemonTask_MissingMount(t *testing.T) { - // mock mkdirAllAndChown - origMkdir := mkdirAllAndChown - defer func() { mkdirAllAndChown = origMkdir }() - mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { - return nil - } - // set up test managed daemon - tmd := md.NewManagedDaemon(TestDaemonName, TestImageTag) - tmd.SetLoadedDaemonImageRef(TestImageRef) - // test failure with a missing applicationLogMount - testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} - testOtherMount := &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/"} - testMountPoints := []*md.MountPoint{} - testMountPoints = append(testMountPoints, testAgentCommunicationMount, testOtherMount) - tmd.SetMountPoints(testMountPoints) - testDaemonManager := NewDaemonManager(tmd) - _, err := testDaemonManager.CreateDaemonTask() - assert.EqualError(t, err, fmt.Sprintf("%s is an invalid managed daemon", TestDaemonName)) - - // add required log mount but no healthcheck - testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} - testMountPoints = append(testMountPoints, testApplicationLogMount) - tmd.SetMountPoints(testMountPoints) - testDaemonManager = NewDaemonManager(tmd) - _, err = testDaemonManager.CreateDaemonTask() - assert.Nil(t, err) - - // add required healthcheck - testHealthCheck := []string{"test"} - tmd.SetHealthCheck(testHealthCheck, 2*time.Minute, 2*time.Minute, 1) - testDaemonManager = NewDaemonManager(tmd) - resultDaemonTask, err := testDaemonManager.CreateDaemonTask() - - // validate daemon task configs - assert.Equal(t, fmt.Sprintf("arn:::::/%s-", TestDaemonName), resultDaemonTask.Arn[:20], "Task Arn prefix should match Image Name ") - assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") - assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") - assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") - assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInteral should be true") - - // validate task container - assert.Equal(t, TestImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") - - // validate daemon container configs - configRaw := resultDaemonTask.Containers[0].DockerConfig.Config - hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig - var configMap map[string]interface{} - var hostConfigMap map[string]interface{} - json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) - json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) - // validate mount point count - if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { - assert.Equal(t, len(containerMounts), 3, "Task should have Required container binds (2) + 1 other bind") - } else { - t.Errorf("Unable to find 'Mounts' in container definition map") - } - // validate healthcheck - containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) - containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) - assert.Equal(t, testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") -} - -func TestCreateDaemonTask_PrivilegeAndMountPropagation(t *testing.T) { - // mock mkdirAllAndChown - origMkdir := mkdirAllAndChown - defer func() { mkdirAllAndChown = origMkdir }() - mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { - return nil - } - // set up test managed daemon - tmd := md.NewManagedDaemon(TestDaemonName, TestImageTag) - tmd.SetLoadedDaemonImageRef(TestImageRef) - tmd.SetPrivileged(true) - // test failure with a missing applicationLogMount - testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} - testOtherMount := &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: true} - testMountPoints := []*md.MountPoint{} - testMountPoints = append(testMountPoints, testAgentCommunicationMount, testOtherMount) - tmd.SetMountPoints(testMountPoints) - testDaemonManager := NewDaemonManager(tmd) - _, err := testDaemonManager.CreateDaemonTask() - assert.EqualError(t, err, fmt.Sprintf("%s is an invalid managed daemon", TestDaemonName)) - - // add required log mount but no healthcheck - testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} - testMountPoints = append(testMountPoints, testApplicationLogMount) - tmd.SetMountPoints(testMountPoints) - testDaemonManager = NewDaemonManager(tmd) - _, err = testDaemonManager.CreateDaemonTask() - assert.Nil(t, err) - - // add required healthcheck - testHealthCheck := []string{"test"} - tmd.SetHealthCheck(testHealthCheck, 2*time.Minute, 2*time.Minute, 1) - testDaemonManager = NewDaemonManager(tmd) - resultDaemonTask, err := testDaemonManager.CreateDaemonTask() - - // validate daemon task configs - assert.Equal(t, fmt.Sprintf("arn:::::/%s-", TestDaemonName), resultDaemonTask.Arn[:20], "Task Arn prefix should match Image Name ") - assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") - assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") - assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") - assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInteral should be true") - - // validate task container - assert.Equal(t, TestImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") - - // validate daemon container configs - configRaw := resultDaemonTask.Containers[0].DockerConfig.Config - hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig - var configMap map[string]interface{} - var hostConfigMap map[string]interface{} - json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) - json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) - // validate mount point has One mount with Shared Propagation - if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { - res := strings.Count(fmt.Sprintf("%v", containerMounts), "Propagation:shared") - assert.Equal(t, res, 1, "Task should have only one mount with shared Propagation") - } else { - t.Errorf("missing 'Mounts' in hostConfigMap") - } - // validate privileged - containerPrivileged := hostConfigMap["Privileged"].(bool) - assert.True(t, containerPrivileged, "Daemon Container should be privileged") - // validate healthcheck - containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) - containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) - assert.Equal(t, testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") -} - -// containsString will typecast elements to strings and compare to the target -func containsString(arr []interface{}, target string) bool { - for _, val := range arr { - if val.(string) == target { - return true - } - } - return false -} diff --git a/agent/engine/daemonmanager/daemon_manager_test.go b/agent/engine/daemonmanager/daemon_manager_test.go new file mode 100644 index 00000000000..2ca7153da62 --- /dev/null +++ b/agent/engine/daemonmanager/daemon_manager_test.go @@ -0,0 +1,269 @@ +//go:build unit +// +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 daemonmanager + +import ( + "encoding/json" + "fmt" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + apitaskstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status" + md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" + "io/fs" + "strings" + "testing" + "time" +) + +func TestCreateDaemonTask(t *testing.T) { + + cases := []struct { + testName string + testDaemonName string + testImageRef string + testOtherMount *md.MountPoint + testHealthCheck []string + }{ + { + testName: "Basic Daemon", + testDaemonName: TestDaemonName, + testImageRef: TestImageRef, + testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, + testHealthCheck: []string{TestHealthString}, + }, + { + testName: "Daemon Updated Daemon Name", + testDaemonName: "TestDeemen", + testImageRef: TestImageRef, + testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, + testHealthCheck: []string{TestHealthString}, + }, + { + testName: "Daemon Updated Image ref", + testDaemonName: TestDaemonName, + testImageRef: "TestOtherImage", + testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, + testHealthCheck: []string{TestHealthString}, + }, + { + testName: "Daemon With Updated Mounts", + testDaemonName: TestDaemonName, + testImageRef: TestImageRef, + testOtherMount: &md.MountPoint{SourceVolumeID: "testUpdatedMountVolume", ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: false}, + testHealthCheck: []string{TestHealthString}, + }, + { + testName: "Daemon With Updated HealthCheck", + testDaemonName: TestDaemonName, + testImageRef: TestImageRef, + testOtherMount: &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/"}, + testHealthCheck: []string{"testOtherHealth"}, + }, + } + + for _, c := range cases { + t.Run(c.testName, func(t *testing.T) { + // mock mkdirAllAndChown + origMkdir := mkdirAllAndChown + defer func() { mkdirAllAndChown = origMkdir }() + mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { + return nil + } + // set up test managed daemon + tmd := md.NewManagedDaemon(c.testDaemonName, TestImageTag) + tmd.SetLoadedDaemonImageRef(c.testImageRef) + // create required mounts and add all + testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} + testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} + testMountPoints := []*md.MountPoint{} + testMountPoints = append(testMountPoints, testAgentCommunicationMount, testApplicationLogMount, c.testOtherMount) + tmd.SetMountPoints(testMountPoints) + // add required healthcheck + tmd.SetHealthCheck(c.testHealthCheck, 2*time.Minute, 2*time.Minute, 1) + // create daemon manager for the above daemon + testDaemonManager := NewDaemonManager(tmd) + resultDaemonTask, _ := testDaemonManager.CreateDaemonTask() + // validate daemon task configs + assert.Equal(t, fmt.Sprintf("arn:::::/%s-", c.testDaemonName), strings.Split(resultDaemonTask.Arn, "-")[0]+"-", "Task Arn prefix should match Image Name ") + assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") + assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") + assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") + assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInternal should be true") + // validate task container + assert.Equal(t, c.testImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") + // validate daemon container configs + configRaw := resultDaemonTask.Containers[0].DockerConfig.Config + hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig + var configMap map[string]interface{} + var hostConfigMap map[string]interface{} + json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) + json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) + // validate mount point count + if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { + assert.Equal(t, len(containerMounts), 3, "Task should have Required container binds (2) + 1 other bind") + } else { + t.Errorf("Unable to find 'Mounts' in container definition map") + } + // validate healthcheck + containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) + containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) + assert.Equal(t, c.testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") + }) + } +} + +func TestFailCreateDaemonTask_MissingMount(t *testing.T) { + // mock mkdirAllAndChown + origMkdir := mkdirAllAndChown + defer func() { mkdirAllAndChown = origMkdir }() + mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { + return nil + } + // set up test managed daemon + tmd := md.NewManagedDaemon(TestDaemonName, TestImageTag) + tmd.SetLoadedDaemonImageRef(TestImageRef) + // test failure with a missing applicationLogMount + testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} + testOtherMount := &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/"} + testMountPoints := []*md.MountPoint{} + testMountPoints = append(testMountPoints, testAgentCommunicationMount, testOtherMount) + tmd.SetMountPoints(testMountPoints) + testDaemonManager := NewDaemonManager(tmd) + _, err := testDaemonManager.CreateDaemonTask() + assert.EqualError(t, err, fmt.Sprintf("%s is an invalid managed daemon", TestDaemonName)) + + // add required log mount but no healthcheck + testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} + testMountPoints = append(testMountPoints, testApplicationLogMount) + tmd.SetMountPoints(testMountPoints) + testDaemonManager = NewDaemonManager(tmd) + _, err = testDaemonManager.CreateDaemonTask() + assert.Nil(t, err) + + // add required healthcheck + testHealthCheck := []string{"test"} + tmd.SetHealthCheck(testHealthCheck, 2*time.Minute, 2*time.Minute, 1) + testDaemonManager = NewDaemonManager(tmd) + resultDaemonTask, err := testDaemonManager.CreateDaemonTask() + + // validate daemon task configs + assert.Equal(t, fmt.Sprintf("arn:::::/%s-", TestDaemonName), strings.Split(resultDaemonTask.Arn, "-")[0]+"-", "Task Arn prefix should match Image Name ") + assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") + assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") + assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") + assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInternal should be true") + + // validate task container + assert.Equal(t, TestImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") + + // validate daemon container configs + configRaw := resultDaemonTask.Containers[0].DockerConfig.Config + hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig + var configMap map[string]interface{} + var hostConfigMap map[string]interface{} + json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) + json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) + // validate mount point count + if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { + assert.Equal(t, len(containerMounts), 3, "Task should have Required container binds (2) + 1 other bind") + } else { + t.Errorf("Unable to find 'Mounts' in container definition map") + } + // validate healthcheck + containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) + containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) + assert.Equal(t, testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") +} + +func TestCreateDaemonTask_PrivilegeAndMountPropagation(t *testing.T) { + // mock mkdirAllAndChown + origMkdir := mkdirAllAndChown + defer func() { mkdirAllAndChown = origMkdir }() + mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { + return nil + } + // set up test managed daemon + tmd := md.NewManagedDaemon(TestDaemonName, TestImageTag) + tmd.SetLoadedDaemonImageRef(TestImageRef) + tmd.SetPrivileged(true) + // test failure with a missing applicationLogMount + testAgentCommunicationMount := &md.MountPoint{SourceVolumeID: "agentCommunicationMount", ContainerPath: "/container/run/"} + testOtherMount := &md.MountPoint{SourceVolumeID: TestOtherVolumeID, ContainerPath: "/container/other/", SourceVolumeHostPath: "/var/ecs/other/", PropagationShared: true} + testMountPoints := []*md.MountPoint{} + testMountPoints = append(testMountPoints, testAgentCommunicationMount, testOtherMount) + tmd.SetMountPoints(testMountPoints) + testDaemonManager := NewDaemonManager(tmd) + _, err := testDaemonManager.CreateDaemonTask() + assert.EqualError(t, err, fmt.Sprintf("%s is an invalid managed daemon", TestDaemonName)) + + // add required log mount but no healthcheck + testApplicationLogMount := &md.MountPoint{SourceVolumeID: "applicationLogMount", ContainerPath: "/container/log/"} + testMountPoints = append(testMountPoints, testApplicationLogMount) + tmd.SetMountPoints(testMountPoints) + testDaemonManager = NewDaemonManager(tmd) + _, err = testDaemonManager.CreateDaemonTask() + assert.Nil(t, err) + + // add required healthcheck + testHealthCheck := []string{"test"} + tmd.SetHealthCheck(testHealthCheck, 2*time.Minute, 2*time.Minute, 1) + testDaemonManager = NewDaemonManager(tmd) + resultDaemonTask, err := testDaemonManager.CreateDaemonTask() + + // validate daemon task configs + assert.Equal(t, fmt.Sprintf("arn:::::/%s-", TestDaemonName), strings.Split(resultDaemonTask.Arn, "-")[0]+"-", "Task Arn prefix should match Image Name ") + assert.Equal(t, apitaskstatus.TaskRunning, resultDaemonTask.DesiredStatusUnsafe, "Task DesiredStatus should be running") + assert.Equal(t, apitask.HostNetworkMode, resultDaemonTask.NetworkMode, "Task NetworkMode should be Host") + assert.Equal(t, "EC2", resultDaemonTask.LaunchType, "Task LaunchType should be EC2") + assert.Equal(t, true, resultDaemonTask.IsInternal, "Task IsInteral should be true") + + // validate task container + assert.Equal(t, TestImageRef, resultDaemonTask.Containers[0].Image, "Task Container Image Name should match image ref") + + // validate daemon container configs + configRaw := resultDaemonTask.Containers[0].DockerConfig.Config + hostConfigRaw := resultDaemonTask.Containers[0].DockerConfig.HostConfig + var configMap map[string]interface{} + var hostConfigMap map[string]interface{} + json.Unmarshal([]byte(aws.StringValue(configRaw)), &configMap) + json.Unmarshal([]byte(aws.StringValue(hostConfigRaw)), &hostConfigMap) + // validate mount point has One mount with Shared Propagation + if containerMounts, ok := hostConfigMap["Mounts"].([]interface{}); ok { + res := strings.Count(fmt.Sprintf("%v", containerMounts), "Propagation:shared") + assert.Equal(t, res, 1, "Task should have only one mount with shared Propagation") + } else { + t.Errorf("missing 'Mounts' in hostConfigMap") + } + // validate privileged + containerPrivileged := hostConfigMap["Privileged"].(bool) + assert.True(t, containerPrivileged, "Daemon Container should be privileged") + // validate healthcheck + containerHealthCheck := configMap["Healthcheck"].(map[string]interface{}) + containerHealthCheckTest := containerHealthCheck["Test"].([]interface{}) + assert.Equal(t, testHealthCheck[0], containerHealthCheckTest[0].(string), "Container health check has changed") +} + +// containsString will typecast elements to strings and compare to the target +func containsString(arr []interface{}, target string) bool { + for _, val := range arr { + if val.(string) == target { + return true + } + } + return false +} diff --git a/agent/engine/daemonmanager/daemon_manager_windows.go b/agent/engine/daemonmanager/daemon_manager_windows.go index d051ad6a353..a62f07c7b60 100644 --- a/agent/engine/daemonmanager/daemon_manager_windows.go +++ b/agent/engine/daemonmanager/daemon_manager_windows.go @@ -17,54 +17,15 @@ package daemonmanager import ( - "context" - "errors" - "fmt" - "os" - - apitask "github.com/aws/amazon-ecs-agent/agent/api/task" - "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" - "github.com/aws/amazon-ecs-agent/agent/utils" - "github.com/aws/amazon-ecs-agent/agent/utils/loader" - "github.com/aws/amazon-ecs-agent/ecs-agent/logger" - "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" - "github.com/docker/docker/api/types" + "io/fs" ) -func (dm *daemonManager) CreateDaemonTask() (*apitask.Task, error) { - return nil, errors.New("daemonmanager.CreateDaemonTask not implemented for Windows") -} - -// LoadImage loads the daemon's latest image -func (dm *daemonManager) LoadImage(ctx context.Context, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - var loadErr error - daemonImageToLoad := dm.managedDaemon.GetImageName() - daemonImageTarPath := dm.managedDaemon.GetImageTarPath() - if _, err := os.Stat(daemonImageTarPath); err != nil { - logger.Warn(fmt.Sprintf("%s container tarball unavailable at path: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ - field.Error: err, - }) - } - logger.Debug(fmt.Sprintf("Loading %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath)) - if loadErr = loader.LoadFromFile(ctx, daemonImageTarPath, dockerClient); loadErr != nil { - logger.Warn(fmt.Sprintf("Unable to load %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), logger.Fields{ - field.Error: loadErr, - }) - } - dm.managedDaemon.SetLoadedDaemonImageRef(dm.managedDaemon.GetImageRef()) - loadedImageRef := dm.managedDaemon.GetLoadedDaemonImageRef() - logger.Info(fmt.Sprintf("Successfully loaded %s container image from tarball: %s", daemonImageToLoad, daemonImageTarPath), - logger.Fields{ - field.Image: loadedImageRef, - }) - return loader.GetContainerImage(loadedImageRef, dockerClient) -} - -func (dm *daemonManager) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - return loader.IsImageLoaded(dm.managedDaemon.GetImageRef(), dockerClient) -} - -// Returns true if the Daemon image is found on this host, false otherwise. -func (dm *daemonManager) ImageExists() (bool, error) { - return utils.FileExists(dm.managedDaemon.GetImageTarPath()) -} +const ( + daemonUID = 0 + daemonMountPermission fs.FileMode = 0755 + daemonLogPermission fs.FileMode = 0777 + ecsAgentLogFileENV = "ECS_LOGFILE" + defaultECSAgentLogPathContainer = "/log" + socketPathHostRoot = "C:\\ProgramData\\Amazon\\ECS\\ebs-csi-driver" + logPathHostRoot = "C:\\ProgramData\\Amazon\\ECS\\log\\daemons" +) diff --git a/agent/engine/daemonmanager/daemon_manager_windows_test.go b/agent/engine/daemonmanager/daemon_manager_windows_test.go new file mode 100644 index 00000000000..ab9bdec3f94 --- /dev/null +++ b/agent/engine/daemonmanager/daemon_manager_windows_test.go @@ -0,0 +1,28 @@ +//go:build windows && unit +// +build windows,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 daemonmanager + +const ( + TestDaemonName = "TestWindowsDaemon" + TestImageRef = "TestWindowsImage" + TestImageTag = "testWindowsImageTag" + TestOtherVolumeID = "otherTestMount" + TestHealthString = "testHealth" + TestImagePath = "/test/image/path" + TestAgentPath = "/test/agent/path" + TestMountPointVolume = "testVolume" +) diff --git a/agent/utils/utils_windows.go b/agent/utils/utils_windows.go index 938db186bf8..ee8b7709c4e 100644 --- a/agent/utils/utils_windows.go +++ b/agent/utils/utils_windows.go @@ -18,6 +18,8 @@ package utils import ( "errors" + "fmt" + "io/fs" "os" "path/filepath" "regexp" @@ -80,3 +82,18 @@ func IsAvailableDriveLetter(hostPath string) bool { } return false } + +func MkdirAllAndChown(path string, perm fs.FileMode, uid, gid int) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + err = os.MkdirAll(path, perm) + } + if err != nil { + return fmt.Errorf("failed to mkdir %s: %+v", path, err) + } + // ToDo: Fix this. Commenting for now as Chown does not work for Windows + //if err = os.Chown(path, uid, gid); err != nil { + // return fmt.Errorf("failed to chown %s: %+v", path, err) + //} + return nil +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client.go index 91adf1d8c91..7f41a656aa1 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client.go @@ -17,6 +17,7 @@ import ( "context" "fmt" "net" + "path/filepath" "time" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" @@ -32,6 +33,10 @@ const ( fsTypeBlockName = "block" ) +func DefaultSocketFilePath() string { + return filepath.Join(DefaultSocketHostPath, DefaultImageName, DefaultSocketName) +} + // CSIClient is an interface that specifies all supported operations in the Container Storage Interface(CSI) // driver for Agent uses. The CSI driver provides many volume related operations to manage the lifecycle of // Amazon EBS volumes, including mounting, umounting, resizing and volume stats. @@ -63,7 +68,8 @@ func NewCSIClient(socketIn string) csiClient { } // Returns a CSI client configured with default settings. -// The default socket filepath is "/var/run/ecs/ebs-csi-driver/csi-driver.sock". +// The default socket filepath is defined in the respective DefaultSocketFilePath method +// for each platform (linux/windows). func NewDefaultCSIClient() CSIClient { client := NewCSIClient(DefaultSocketFilePath()) return &client diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_linux.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_linux.go index 334a7ed3e20..890afffd5d6 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_linux.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_linux.go @@ -16,8 +16,6 @@ package csiclient import ( - "path/filepath" - md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" ) @@ -26,7 +24,3 @@ const ( DefaultSocketName = "csi-driver.sock" DefaultSocketHostPath = "/var/run/ecs/" ) - -func DefaultSocketFilePath() string { - return filepath.Join(DefaultSocketHostPath, DefaultImageName, DefaultSocketName) -} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_windows.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_windows.go index 13221fab8dd..0277c7d63d8 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_windows.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/csiclient/csi_client_windows.go @@ -15,12 +15,10 @@ // permissions and limitations under the License. package csiclient +import md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" + const ( - DefaultImageName = "" - DefaultSocketName = "" - DefaultSocketHostPath = "" + DefaultImageName = md.EbsCsiDriver + DefaultSocketName = "csi-driver.sock" + DefaultSocketHostPath = "C:\\ProgramData\\Amazon\\ECS\\" ) - -func DefaultSocketFilePath() string { - return "unimplemented" // TODO: Windows implementation -} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon.go index 3b615e40697..63aca282339 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon.go @@ -15,7 +15,6 @@ package manageddaemon import ( "fmt" - "os" "path/filepath" "time" @@ -25,12 +24,7 @@ import ( ) const ( - imageTarPath = "/var/lib/ecs/deps/daemons" - imageTagDefault = "latest" - defaultAgentCommunicationPathHostRoot = "/var/run/ecs" - defaultApplicationLogPathHostRoot = "/var/log/ecs/daemons" - defaultAgentCommunicationMount = "agentCommunicationMount" - defaultApplicationLogMount = "applicationLogMount" + imageFileName = "ebs-csi-driver.tar" ) type ManagedDaemon struct { @@ -89,70 +83,6 @@ func NewManagedDaemon( var ImportAll = defaultImportAll -// ImportAll function will parse/validate all managed daemon definitions -// defined in /var/lib/ecs/deps/daemons and will return an array -// of valid ManagedDeamon objects -func defaultImportAll() ([]*ManagedDaemon, error) { - // TODO parse taskdef json files in parameterized dir ie /deps/daemons - // TODO validate that each daemon's layers are loaded or that daemon has a corresponding image tar - ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, "ebs-csi-driver.tar") - if _, err := os.Stat(ebsCsiTarFile); err != nil { - return []*ManagedDaemon{}, nil - } - // found the EBS CSI tar file -- import - ebsManagedDaemon := NewManagedDaemon(EbsCsiDriver, "latest") - // add required mounts - ebsMounts := []*MountPoint{ - &MountPoint{ - SourceVolumeID: "agentCommunicationMount", - SourceVolume: "agentCommunicationMount", - SourceVolumeType: "host", - SourceVolumeHostPath: "/var/run/ecs/ebs-csi-driver/", - ContainerPath: "/csi-driver/", - }, - &MountPoint{ - SourceVolumeID: "applicationLogMount", - SourceVolume: "applicationLogMount", - SourceVolumeType: "host", - SourceVolumeHostPath: "/var/log/ecs/daemons/ebs-csi-driver/", - ContainerPath: "/var/log/", - }, - &MountPoint{ - SourceVolumeID: "sharedMounts", - SourceVolume: "sharedMounts", - SourceVolumeType: "host", - SourceVolumeHostPath: "/mnt/ecs/ebs", - ContainerPath: "/mnt/ecs/ebs", - PropagationShared: true, - }, - &MountPoint{ - SourceVolumeID: "devMount", - SourceVolume: "devMount", - SourceVolumeType: "host", - SourceVolumeHostPath: "/dev", - ContainerPath: "/dev", - PropagationShared: true, - }, - } - if err := ebsManagedDaemon.SetMountPoints(ebsMounts); err != nil { - return nil, fmt.Errorf("Unable to import EBS ManagedDaemon: %s", err) - } - var thisCommand []string - thisCommand = append(thisCommand, "--endpoint=unix://csi-driver/csi-driver.sock") - thisCommand = append(thisCommand, "--log_file=/var/log/csi.log") - thisCommand = append(thisCommand, "--log_file_max_size=20") - thisCommand = append(thisCommand, "--logtostderr=false") - sysAdmin := "SYS_ADMIN" - addCapabilities := []*string{&sysAdmin} - kernelCapabilities := ecsacs.KernelCapabilities{Add: addCapabilities} - ebsLinuxParams := ecsacs.LinuxParameters{Capabilities: &kernelCapabilities} - ebsManagedDaemon.linuxParameters = &ebsLinuxParams - - ebsManagedDaemon.command = thisCommand - ebsManagedDaemon.privileged = true - return []*ManagedDaemon{ebsManagedDaemon}, nil -} - func (md *ManagedDaemon) GetLinuxParameters() *ecsacs.LinuxParameters { return md.linuxParameters } @@ -261,10 +191,10 @@ func (md *ManagedDaemon) SetMountPoints(mountPoints []*MountPoint) error { var mountPointMap = make(map[string]*MountPoint) for _, mp := range mountPoints { if mp.SourceVolumeID == defaultAgentCommunicationMount { - mp.SourceVolumeHostPath = fmt.Sprintf("%s/%s/", defaultAgentCommunicationPathHostRoot, md.imageName) + mp.SourceVolumeHostPath = filepath.Join(defaultAgentCommunicationPathHostRoot, md.imageName) md.agentCommunicationMount = mp } else if mp.SourceVolumeID == defaultApplicationLogMount { - mp.SourceVolumeHostPath = fmt.Sprintf("%s/%s/", defaultApplicationLogPathHostRoot, md.imageName) + mp.SourceVolumeHostPath = filepath.Join(defaultApplicationLogPathHostRoot, md.imageName) md.applicationLogMount = mp } else { mountPointMap[mp.SourceVolumeID] = mp diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_linux.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_linux.go new file mode 100644 index 00000000000..bcab2353380 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_linux.go @@ -0,0 +1,98 @@ +//go:build linux +// +build linux + +// 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 manageddaemon + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" +) + +const ( + imageTarPath = "/var/lib/ecs/deps/daemons" + imageTagDefault = "latest" + defaultAgentCommunicationPathHostRoot = "/var/run/ecs" + defaultApplicationLogPathHostRoot = "/var/log/ecs/daemons" + defaultAgentCommunicationMount = "agentCommunicationMount" + defaultApplicationLogMount = "applicationLogMount" +) + +// ImportAll function will parse/validate all managed daemon definitions +// defined in /var/lib/ecs/deps/daemons and will return an array +// of valid ManagedDeamon objects +func defaultImportAll() ([]*ManagedDaemon, error) { + // TODO parse taskdef json files in parameterized dir ie /deps/daemons + // TODO validate that each daemon's layers are loaded or that daemon has a corresponding image tar + ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, imageFileName) + if _, err := os.Stat(ebsCsiTarFile); err != nil { + return []*ManagedDaemon{}, nil + } + // found the EBS CSI tar file -- import + ebsManagedDaemon := NewManagedDaemon(EbsCsiDriver, "latest") + // add required mounts + ebsMounts := []*MountPoint{ + &MountPoint{ + SourceVolumeID: "agentCommunicationMount", + SourceVolume: "agentCommunicationMount", + SourceVolumeType: "host", + SourceVolumeHostPath: "/var/run/ecs/ebs-csi-driver/", + ContainerPath: "/csi-driver/", + }, + &MountPoint{ + SourceVolumeID: "applicationLogMount", + SourceVolume: "applicationLogMount", + SourceVolumeType: "host", + SourceVolumeHostPath: "/var/log/ecs/daemons/ebs-csi-driver/", + ContainerPath: "/var/log/", + }, + &MountPoint{ + SourceVolumeID: "sharedMounts", + SourceVolume: "sharedMounts", + SourceVolumeType: "host", + SourceVolumeHostPath: "/mnt/ecs/ebs", + ContainerPath: "/mnt/ecs/ebs", + PropagationShared: true, + }, + &MountPoint{ + SourceVolumeID: "devMount", + SourceVolume: "devMount", + SourceVolumeType: "host", + SourceVolumeHostPath: "/dev", + ContainerPath: "/dev", + PropagationShared: true, + }, + } + if err := ebsManagedDaemon.SetMountPoints(ebsMounts); err != nil { + return nil, fmt.Errorf("Unable to import EBS ManagedDaemon: %s", err) + } + var thisCommand []string + thisCommand = append(thisCommand, "--endpoint=unix://csi-driver/csi-driver.sock") + thisCommand = append(thisCommand, "--log_file=/var/log/csi.log") + thisCommand = append(thisCommand, "--log_file_max_size=20") + thisCommand = append(thisCommand, "--logtostderr=false") + sysAdmin := "SYS_ADMIN" + addCapabilities := []*string{&sysAdmin} + kernelCapabilities := ecsacs.KernelCapabilities{Add: addCapabilities} + ebsLinuxParams := ecsacs.LinuxParameters{Capabilities: &kernelCapabilities} + ebsManagedDaemon.linuxParameters = &ebsLinuxParams + + ebsManagedDaemon.command = thisCommand + ebsManagedDaemon.privileged = true + return []*ManagedDaemon{ebsManagedDaemon}, nil +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_windows.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_windows.go new file mode 100644 index 00000000000..754fd21ab44 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon/managed_daemon_windows.go @@ -0,0 +1,106 @@ +//go:build windows +// +build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package manageddaemon + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + imageTarPath = "C:\\ProgramData\\Amazon\\ECS\\data\\" + imageTagDefault = "latest" + defaultAgentCommunicationPathHostRoot = "C:\\ProgramData\\Amazon\\ECS" + defaultApplicationLogPathHostRoot = "C:\\ProgramData\\Amazon\\ECS\\log" + defaultAgentCommunicationMount = "agentCommunicationMount" + defaultApplicationLogMount = "applicationLogMount" +) + +// defaultImportAll function will parse/validate all managed daemon definitions +// defined and will return an array of valid ManagedDaemon objects +func defaultImportAll() ([]*ManagedDaemon, error) { + ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, imageFileName) + if _, err := os.Stat(ebsCsiTarFile); err != nil { + return []*ManagedDaemon{}, nil + } + // locate the EBS CSI tar file -- import + ebsManagedDaemon := NewManagedDaemon(EbsCsiDriver, "latest") + // add required mounts + ebsMounts := []*MountPoint{ + &MountPoint{ + SourceVolumeID: "agentCommunicationMount", + SourceVolume: "agentCommunicationMount", + SourceVolumeType: "host", + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\ebs-csi-driver", + ContainerPath: "C:\\ebs-csi-driver\\", + }, + &MountPoint{ + SourceVolumeID: "applicationLogMount", + SourceVolume: "applicationLogMount", + SourceVolumeType: "host", + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\log\\daemons", + ContainerPath: "C:\\csi-driver\\log", + }, + &MountPoint{ + SourceVolumeID: "sharedMounts", + SourceVolume: "sharedMounts", + SourceVolumeType: "host", + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\ebs", + ContainerPath: "C:\\csi-driver\\ebs\\", + PropagationShared: false, + }, + // the following three mount points are for connecting to the CSIProxy server that is running + // as a Windows Service on the host + &MountPoint{ + SourceVolumeID: "csiProxyDiskMount", + SourceVolume: "csiProxyDiskMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-disk-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-disk-v1", + PropagationShared: false, + }, + &MountPoint{ + SourceVolumeID: "csiProxyFileSystemMount", + SourceVolume: "csiProxyFileSystemMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-filesystem-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-filesystem-v1", + PropagationShared: false, + }, + &MountPoint{ + SourceVolumeID: "csiProxyVolumeMount", + SourceVolume: "csiProxyVolumeMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-volume-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-volume-v1", + PropagationShared: false, + }, + } + if err := ebsManagedDaemon.SetMountPoints(ebsMounts); err != nil { + return nil, fmt.Errorf("Unable to import EBS ManagedDaemon: %s", err) + } + var thisCommand []string + thisCommand = append(thisCommand, "-endpoint=unix://ebs-csi-driver/csi-driver.sock") + thisCommand = append(thisCommand, "-log_file=C:\\csi-driver\\log\\csi.log") + thisCommand = append(thisCommand, "-log_file_max_size=20") + thisCommand = append(thisCommand, "-logtostderr=false") + + ebsManagedDaemon.command = thisCommand + ebsManagedDaemon.privileged = false + return []*ManagedDaemon{ebsManagedDaemon}, nil +} diff --git a/ecs-agent/csiclient/csi_client.go b/ecs-agent/csiclient/csi_client.go index 91adf1d8c91..7f41a656aa1 100644 --- a/ecs-agent/csiclient/csi_client.go +++ b/ecs-agent/csiclient/csi_client.go @@ -17,6 +17,7 @@ import ( "context" "fmt" "net" + "path/filepath" "time" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" @@ -32,6 +33,10 @@ const ( fsTypeBlockName = "block" ) +func DefaultSocketFilePath() string { + return filepath.Join(DefaultSocketHostPath, DefaultImageName, DefaultSocketName) +} + // CSIClient is an interface that specifies all supported operations in the Container Storage Interface(CSI) // driver for Agent uses. The CSI driver provides many volume related operations to manage the lifecycle of // Amazon EBS volumes, including mounting, umounting, resizing and volume stats. @@ -63,7 +68,8 @@ func NewCSIClient(socketIn string) csiClient { } // Returns a CSI client configured with default settings. -// The default socket filepath is "/var/run/ecs/ebs-csi-driver/csi-driver.sock". +// The default socket filepath is defined in the respective DefaultSocketFilePath method +// for each platform (linux/windows). func NewDefaultCSIClient() CSIClient { client := NewCSIClient(DefaultSocketFilePath()) return &client diff --git a/ecs-agent/csiclient/csi_client_linux.go b/ecs-agent/csiclient/csi_client_linux.go index 334a7ed3e20..890afffd5d6 100644 --- a/ecs-agent/csiclient/csi_client_linux.go +++ b/ecs-agent/csiclient/csi_client_linux.go @@ -16,8 +16,6 @@ package csiclient import ( - "path/filepath" - md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" ) @@ -26,7 +24,3 @@ const ( DefaultSocketName = "csi-driver.sock" DefaultSocketHostPath = "/var/run/ecs/" ) - -func DefaultSocketFilePath() string { - return filepath.Join(DefaultSocketHostPath, DefaultImageName, DefaultSocketName) -} diff --git a/ecs-agent/csiclient/csi_client_windows.go b/ecs-agent/csiclient/csi_client_windows.go index 13221fab8dd..0277c7d63d8 100644 --- a/ecs-agent/csiclient/csi_client_windows.go +++ b/ecs-agent/csiclient/csi_client_windows.go @@ -15,12 +15,10 @@ // permissions and limitations under the License. package csiclient +import md "github.com/aws/amazon-ecs-agent/ecs-agent/manageddaemon" + const ( - DefaultImageName = "" - DefaultSocketName = "" - DefaultSocketHostPath = "" + DefaultImageName = md.EbsCsiDriver + DefaultSocketName = "csi-driver.sock" + DefaultSocketHostPath = "C:\\ProgramData\\Amazon\\ECS\\" ) - -func DefaultSocketFilePath() string { - return "unimplemented" // TODO: Windows implementation -} diff --git a/ecs-agent/manageddaemon/managed_daemon.go b/ecs-agent/manageddaemon/managed_daemon.go index cc9a0e96703..63aca282339 100644 --- a/ecs-agent/manageddaemon/managed_daemon.go +++ b/ecs-agent/manageddaemon/managed_daemon.go @@ -15,6 +15,7 @@ package manageddaemon import ( "fmt" + "path/filepath" "time" "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" @@ -22,6 +23,10 @@ import ( dockercontainer "github.com/docker/docker/api/types/container" ) +const ( + imageFileName = "ebs-csi-driver.tar" +) + type ManagedDaemon struct { imageName string imageTag string @@ -186,10 +191,10 @@ func (md *ManagedDaemon) SetMountPoints(mountPoints []*MountPoint) error { var mountPointMap = make(map[string]*MountPoint) for _, mp := range mountPoints { if mp.SourceVolumeID == defaultAgentCommunicationMount { - mp.SourceVolumeHostPath = fmt.Sprintf("%s/%s/", defaultAgentCommunicationPathHostRoot, md.imageName) + mp.SourceVolumeHostPath = filepath.Join(defaultAgentCommunicationPathHostRoot, md.imageName) md.agentCommunicationMount = mp } else if mp.SourceVolumeID == defaultApplicationLogMount { - mp.SourceVolumeHostPath = fmt.Sprintf("%s/%s/", defaultApplicationLogPathHostRoot, md.imageName) + mp.SourceVolumeHostPath = filepath.Join(defaultApplicationLogPathHostRoot, md.imageName) md.applicationLogMount = mp } else { mountPointMap[mp.SourceVolumeID] = mp diff --git a/ecs-agent/manageddaemon/managed_daemon_linux.go b/ecs-agent/manageddaemon/managed_daemon_linux.go index 4cf677404ca..bcab2353380 100644 --- a/ecs-agent/manageddaemon/managed_daemon_linux.go +++ b/ecs-agent/manageddaemon/managed_daemon_linux.go @@ -39,7 +39,7 @@ const ( func defaultImportAll() ([]*ManagedDaemon, error) { // TODO parse taskdef json files in parameterized dir ie /deps/daemons // TODO validate that each daemon's layers are loaded or that daemon has a corresponding image tar - ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, "ebs-csi-driver.tar") + ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, imageFileName) if _, err := os.Stat(ebsCsiTarFile); err != nil { return []*ManagedDaemon{}, nil } diff --git a/ecs-agent/manageddaemon/managed_daemon_linux_test.go b/ecs-agent/manageddaemon/managed_daemon_linux_test.go new file mode 100644 index 00000000000..622b8478892 --- /dev/null +++ b/ecs-agent/manageddaemon/managed_daemon_linux_test.go @@ -0,0 +1,27 @@ +//go:build linux && unit +// +build linux,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 manageddaemon + +const ( + TestImageName = "TestDaemon" + TestImageTag = "testTag" + TestImagePath = "/test/image/path" + TestAgentPath = "/test/agent/path" + TestMountPointVolume = "testVolume" + ExpectedAgentCommunicationMountFormat = "/var/run/ecs/%s/" + ExpectedApplicationLogMountFormat = "/var/log/ecs/daemons/%s/" +) diff --git a/ecs-agent/manageddaemon/managed_daemon_test.go b/ecs-agent/manageddaemon/managed_daemon_test.go index 60e000a02cc..16cf3dc59e9 100644 --- a/ecs-agent/manageddaemon/managed_daemon_test.go +++ b/ecs-agent/manageddaemon/managed_daemon_test.go @@ -25,14 +25,6 @@ import ( "github.com/stretchr/testify/assert" ) -const ( - TestImageName = "TestDaemon" - TestImageTag = "testTag" - TestImagePath = "/test/image/path" - TestAgentPath = "/test/agent/path" - TestMountPointVolume = "testVolume" -) - func TestNewManagedDaemon(t *testing.T) { cases := []struct { testName string @@ -113,8 +105,8 @@ func TestSetMountPoints(t *testing.T) { tmd.SetMountPoints(mountPoints) assert.Equal(t, c.ExpectedMountCount, len(tmd.GetFilteredMountPoints()), "Wrong value for Set Managed Daemon Mounts") // validate required mount points - expectedAgentCommunicationMount := fmt.Sprintf("/var/run/ecs/%s/", TestImageName) - expectedApplicationLogMount := fmt.Sprintf("/var/log/ecs/daemons/%s/", TestImageName) + expectedAgentCommunicationMount := fmt.Sprintf(ExpectedAgentCommunicationMountFormat, TestImageName) + expectedApplicationLogMount := fmt.Sprintf(ExpectedApplicationLogMountFormat, TestImageName) assert.Equal(t, expectedAgentCommunicationMount, tmd.GetAgentCommunicationMount().SourceVolumeHostPath) assert.Equal(t, expectedApplicationLogMount, tmd.GetApplicationLogMount().SourceVolumeHostPath) }) diff --git a/ecs-agent/manageddaemon/managed_daemon_windows.go b/ecs-agent/manageddaemon/managed_daemon_windows.go index b70f269af60..754fd21ab44 100644 --- a/ecs-agent/manageddaemon/managed_daemon_windows.go +++ b/ecs-agent/manageddaemon/managed_daemon_windows.go @@ -1,34 +1,44 @@ //go:build windows // +build windows +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + package manageddaemon import ( "fmt" "os" "path/filepath" - - "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" ) const ( - imageTarPath = "C:\\ProgramFiles\\Amazon" + imageTarPath = "C:\\ProgramData\\Amazon\\ECS\\data\\" imageTagDefault = "latest" - defaultAgentCommunicationPathHostRoot = "/var/run/ecs" - defaultApplicationLogPathHostRoot = "/var/log/ecs/daemons" + defaultAgentCommunicationPathHostRoot = "C:\\ProgramData\\Amazon\\ECS" + defaultApplicationLogPathHostRoot = "C:\\ProgramData\\Amazon\\ECS\\log" defaultAgentCommunicationMount = "agentCommunicationMount" defaultApplicationLogMount = "applicationLogMount" ) -// ImportAll function will parse/validate all managed daemon definitions -// defined in /var/lib/ecs/deps/daemons and will return an array -// of valid ManagedDeamon objects +// defaultImportAll function will parse/validate all managed daemon definitions +// defined and will return an array of valid ManagedDaemon objects func defaultImportAll() ([]*ManagedDaemon, error) { - ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, "ebs-csi-driver.tar") + ebsCsiTarFile := filepath.Join(imageTarPath, EbsCsiDriver, imageFileName) if _, err := os.Stat(ebsCsiTarFile); err != nil { return []*ManagedDaemon{}, nil } - // found the EBS CSI tar file -- import + // locate the EBS CSI tar file -- import ebsManagedDaemon := NewManagedDaemon(EbsCsiDriver, "latest") // add required mounts ebsMounts := []*MountPoint{ @@ -36,48 +46,61 @@ func defaultImportAll() ([]*ManagedDaemon, error) { SourceVolumeID: "agentCommunicationMount", SourceVolume: "agentCommunicationMount", SourceVolumeType: "host", - SourceVolumeHostPath: "/var/run/ecs/ebs-csi-driver/", - ContainerPath: "/csi-driver/", + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\ebs-csi-driver", + ContainerPath: "C:\\ebs-csi-driver\\", }, &MountPoint{ SourceVolumeID: "applicationLogMount", SourceVolume: "applicationLogMount", SourceVolumeType: "host", - SourceVolumeHostPath: "/var/log/ecs/daemons/ebs-csi-driver/", - ContainerPath: "/var/log/", + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\log\\daemons", + ContainerPath: "C:\\csi-driver\\log", }, &MountPoint{ SourceVolumeID: "sharedMounts", SourceVolume: "sharedMounts", SourceVolumeType: "host", - SourceVolumeHostPath: "/mnt/ecs/ebs", - ContainerPath: "/mnt/ecs/ebs", - PropagationShared: true, + SourceVolumeHostPath: "C:\\ProgramData\\Amazon\\ECS\\ebs", + ContainerPath: "C:\\csi-driver\\ebs\\", + PropagationShared: false, }, + // the following three mount points are for connecting to the CSIProxy server that is running + // as a Windows Service on the host &MountPoint{ - SourceVolumeID: "devMount", - SourceVolume: "devMount", - SourceVolumeType: "host", - SourceVolumeHostPath: "/dev", - ContainerPath: "/dev", - PropagationShared: true, + SourceVolumeID: "csiProxyDiskMount", + SourceVolume: "csiProxyDiskMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-disk-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-disk-v1", + PropagationShared: false, + }, + &MountPoint{ + SourceVolumeID: "csiProxyFileSystemMount", + SourceVolume: "csiProxyFileSystemMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-filesystem-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-filesystem-v1", + PropagationShared: false, + }, + &MountPoint{ + SourceVolumeID: "csiProxyVolumeMount", + SourceVolume: "csiProxyVolumeMount", + SourceVolumeType: "npipe", + SourceVolumeHostPath: "\\\\.\\pipe\\csi-proxy-volume-v1", + ContainerPath: "\\\\.\\pipe\\csi-proxy-volume-v1", + PropagationShared: false, }, } if err := ebsManagedDaemon.SetMountPoints(ebsMounts); err != nil { return nil, fmt.Errorf("Unable to import EBS ManagedDaemon: %s", err) } var thisCommand []string - thisCommand = append(thisCommand, "--endpoint=unix://csi-driver/csi-driver.sock") - thisCommand = append(thisCommand, "--log_file=/var/log/csi.log") - thisCommand = append(thisCommand, "--log_file_max_size=20") - thisCommand = append(thisCommand, "--logtostderr=false") - sysAdmin := "SYS_ADMIN" - addCapabilities := []*string{&sysAdmin} - kernelCapabilities := ecsacs.KernelCapabilities{Add: addCapabilities} - ebsLinuxParams := ecsacs.LinuxParameters{Capabilities: &kernelCapabilities} - ebsManagedDaemon.linuxParameters = &ebsLinuxParams + thisCommand = append(thisCommand, "-endpoint=unix://ebs-csi-driver/csi-driver.sock") + thisCommand = append(thisCommand, "-log_file=C:\\csi-driver\\log\\csi.log") + thisCommand = append(thisCommand, "-log_file_max_size=20") + thisCommand = append(thisCommand, "-logtostderr=false") ebsManagedDaemon.command = thisCommand - ebsManagedDaemon.privileged = true + ebsManagedDaemon.privileged = false return []*ManagedDaemon{ebsManagedDaemon}, nil } diff --git a/ecs-agent/manageddaemon/managed_daemon_windows_test.go b/ecs-agent/manageddaemon/managed_daemon_windows_test.go new file mode 100644 index 00000000000..1e1a50d4cc1 --- /dev/null +++ b/ecs-agent/manageddaemon/managed_daemon_windows_test.go @@ -0,0 +1,27 @@ +//go:build windows && unit +// +build windows,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 manageddaemon + +const ( + TestImageName = "TestDaemon" + TestImageTag = "testTag" + TestImagePath = "C:\\ProgramData\\Amazon\\ECS\\data" + TestAgentPath = "C:\\Program Files\\Amazon\\ECS" + TestMountPointVolume = "testVolume" + ExpectedAgentCommunicationMountFormat = "C:\\ProgramData\\Amazon\\ECS\\%s" + ExpectedApplicationLogMountFormat = "C:\\ProgramData\\Amazon\\ECS\\log\\%s" +)