Skip to content

Commit

Permalink
Support "local down" to stop a running local task (#783)
Browse files Browse the repository at this point in the history
* Support "local down" to stop and remove a running local ECS task

* Skip network removal if it doesn't exist

* Add integ test for "local down"
  • Loading branch information
efekarakus committed Jun 6, 2019
1 parent 0faf4cf commit 6d8674e
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 91 deletions.
3 changes: 3 additions & 0 deletions buildspec.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
version: 0.2

phases:
install: # TODO remove this step after supporting pulling the image part of "ecs-cli local up". See ECS-8445.
commands:
- docker pull amazon/amazon-ecs-local-container-endpoints
build:
commands:
- make integ-test
Expand Down
57 changes: 37 additions & 20 deletions ecs-cli/integ/e2e/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
package e2e

import (
"fmt"
"testing"

"github.com/aws/amazon-ecs-cli/ecs-cli/integ"
"github.com/aws/amazon-ecs-cli/ecs-cli/integ/stdout"
"github.com/stretchr/testify/require"
)

Expand All @@ -40,15 +38,7 @@ func TestECSLocal(t *testing.T) {
{
args: []string{"local", "ps"},
execute: func(t *testing.T, args []string) {
// Given
cmd := integ.GetCommand(args)

// When
out, err := cmd.Output()
require.NoErrorf(t, err, "Failed local ps", fmt.Sprintf("args=%v, stdout=%s, err=%v", args, string(out), err))

// Then
stdout := stdout.Stdout(out)
stdout := integ.RunCmd(t, args)
require.Equal(t, 1, len(stdout.Lines()), "Expected only the table header")
stdout.TestHasAllSubstrings(t, []string{
"CONTAINER ID",
Expand All @@ -64,18 +54,45 @@ func TestECSLocal(t *testing.T) {
{
args: []string{"local", "ps", "--json"},
execute: func(t *testing.T, args []string) {
// Given
cmd := integ.GetCommand(args)

// When
out, err := cmd.Output()
require.NoErrorf(t, err, "Failed local ps", fmt.Sprintf("args=%v, stdout=%s, err=%v", args, string(out), err))

// Then
stdout := stdout.Stdout(out)
stdout := integ.RunCmd(t, args)
stdout.TestHasAllSubstrings(t, []string{"[]"})
},
},
{
args: []string{"local", "down"},
execute: func(t *testing.T, args []string) {
stdout := integ.RunCmd(t, args)
stdout.TestHasAllSubstrings(t, []string{
"docker-compose.local.yml does not exist",
"ecs-local-network not found",
})
},
},
},
},
"run a single local ECS task": {
sequence: []commandTest{
{
args: []string{"local", "up"},
execute: func(t *testing.T, args []string) {
stdout := integ.RunCmd(t, args)
stdout.TestHasAllSubstrings(t, []string{
"Created network ecs-local-network",
"Created the amazon-ecs-local-container-endpoints container",
})
},
},
{
args: []string{"local", "down"},
execute: func(t *testing.T, args []string) {
stdout := integ.RunCmd(t, args)
stdout.TestHasAllSubstrings(t, []string{
"Stopped container with name amazon-ecs-local-container-endpoints",
"Removed container with name amazon-ecs-local-container-endpoints",
"Removed network with name ecs-local-network",
})
},
},
},
},
}
Expand Down
13 changes: 13 additions & 0 deletions ecs-cli/integ/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import (
"strings"
"testing"
"time"

"github.com/aws/amazon-ecs-cli/ecs-cli/integ/stdout"
"github.com/stretchr/testify/require"
)

const (
Expand All @@ -46,6 +49,16 @@ func GetCommand(args []string) *exec.Cmd {
return exec.Command(cmdPath, args...)
}

// RunCmd runs a command with args and returns its Stdout.
func RunCmd(t *testing.T, args []string) stdout.Stdout {
cmd := GetCommand(args)

out, err := cmd.Output()
require.NoErrorf(t, err, "Failed running command", fmt.Sprintf("args=%v, stdout=%s, err=%v", args, string(out), err))

return stdout.Stdout(out)
}

// createTempCoverageFile creates a coverage file for a CLI command under $TMPDIR.
func createTempCoverageFile() (string, error) {
tmpfile, err := ioutil.TempFile("", "coverage-*.out")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,30 @@
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package local
package docker

import (
"os"
"time"

"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)

const (
// TimeoutInS is the wait duration for a response from the Docker daemon before returning an error to the user.
TimeoutInS = 30 * time.Second
)

const (
// minDockerAPIVersion is the minimum Docker API version that supports
// both the Local Endpoints container and the Docker API operations used by "local" sub-commands.
// See https://github.com/awslabs/amazon-ecs-local-container-endpoints/blob/3417a48b676c5b215fb9583bcbdc8a0b0e23aa8e/local-container-endpoints/clients/docker/client.go#L30.
minDockerAPIVersion = "1.27"
)

func newDockerClient() *client.Client {
// NewClient returns an object to communicate with the Docker Engine API.
func NewClient() *client.Client {
if os.Getenv("DOCKER_API_VERSION") == "" {
// If the user does not explicitly set the API version, then the SDK can choose
// an API version that's too new for the user's Docker engine.
Expand Down
101 changes: 101 additions & 0 deletions ecs-cli/modules/cli/local/down_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2015-2019 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 local

import (
"os"
"os/exec"
"path/filepath"

"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/docker"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/network"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"golang.org/x/net/context"
)

// TODO These labels should be defined part of the local.Create workflow.
// Refactor to import these constants instead of re-defining them here.
const (
ecsLocalDockerComposeFileName = "docker-compose.local.yml"
)

// Down stops and removes running local ECS tasks.
// If the user stops the last running task in the local network then also remove the network.
func Down(c *cli.Context) error {
defer func() {
client := docker.NewClient()
network.Teardown(client)
}()

if c.Bool(flags.AllFlag) {
return downAllLocalContainers()
}
return downComposeLocalContainers()
}

func downComposeLocalContainers() error {
wd, _ := os.Getwd()
if _, err := os.Stat(filepath.Join(wd, ecsLocalDockerComposeFileName)); os.IsNotExist(err) {
logrus.Warnf("Compose file %s does not exist in current directory", ecsLocalDockerComposeFileName)
return nil
}

logrus.Infof("Running Compose down on %s", ecsLocalDockerComposeFileName)
cmd := exec.Command("docker-compose", "-f", ecsLocalDockerComposeFileName, "down")
_, err := cmd.CombinedOutput()
if err != nil {
logrus.Fatalf("Failed to run docker-compose down due to %v", err)
}

logrus.Info("Stopped and removed containers successfully")
return nil
}

func downAllLocalContainers() error {
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

client := docker.NewClient()
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
Filters: filters.NewArgs(
filters.Arg("label", ecsLocalLabelKey),
),
All: true,
})
if err != nil {
logrus.Fatalf("Failed to list containers with label=%s due to %v", ecsLocalLabelKey, err)
}
if len(containers) == 0 {
logrus.Warn("No running ECS local tasks found")
return nil
}

logrus.Infof("Stop and remove %d container(s)", len(containers))
for _, container := range containers {
if err = client.ContainerStop(ctx, container.ID, nil); err != nil {
logrus.Fatalf("Failed to stop container %s due to %v", container.ID[:maxContainerIDLength], err)
}
logrus.Infof("Stopped container with id %s", container.ID[:maxContainerIDLength])

if err = client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{}); err != nil {
logrus.Fatalf("Failed to remove container %s due to %v", container.ID[:maxContainerIDLength], err)
}
logrus.Infof("Removed container with id %s", container.ID[:maxContainerIDLength])
}
return nil
}
18 changes: 6 additions & 12 deletions ecs-cli/modules/cli/local/network/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
Expand Down Expand Up @@ -70,12 +70,6 @@ const (
localEndpointsContainerName = "amazon-ecs-local-container-endpoints"
)

// Configuration for Docker requests
const (
// Wait duration for a response from the Docker daemon before returning an error to the user.
dockerTimeout = 30 * time.Second
)

// Setup creates a user-defined bridge network with a running Local Container Endpoints container. If the network
// already exists or the container is already running then this function does nothing.
//
Expand All @@ -94,7 +88,7 @@ func setupLocalNetwork(dockerClient networkCreator) {
}

func localNetworkExists(dockerClient networkCreator) bool {
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

_, err := dockerClient.NetworkInspect(ctx, EcsLocalNetworkName, types.NetworkInspectOptions{})
Expand All @@ -109,7 +103,7 @@ func localNetworkExists(dockerClient networkCreator) bool {
}

func createLocalNetwork(dockerClient networkCreator) {
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

logrus.Infof("Creating network: %s...", EcsLocalNetworkName)
Expand All @@ -136,7 +130,7 @@ func setupLocalEndpointsContainer(docker containerStarter) {
// createLocalEndpointsContainer returns the ID of the newly created container.
// If the container already exists, returns the ID of the existing container.
func createLocalEndpointsContainer(dockerClient containerStarter) string {
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

// See First Scenario in https://aws.amazon.com/blogs/compute/a-guide-to-locally-testing-containers-with-amazon-ecs-local-endpoints-and-docker-compose/
Expand Down Expand Up @@ -182,7 +176,7 @@ func createLocalEndpointsContainer(dockerClient containerStarter) string {
}

func localEndpointsContainerID(dockerClient containerStarter) string {
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

resp, err := dockerClient.ContainerList(ctx, types.ContainerListOptions{
Expand All @@ -201,7 +195,7 @@ func localEndpointsContainerID(dockerClient containerStarter) string {
}

func startContainer(dockerClient containerStarter, containerID string) {
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
defer cancel()

// If the container is already running, Docker does not return an error response.
Expand Down
Loading

0 comments on commit 6d8674e

Please sign in to comment.