Skip to content
This repository was archived by the owner on Nov 19, 2025. It is now read-only.

Commit fc87d82

Browse files
committed
Add "local down" command that teardowns the network
1 parent 211d03a commit fc87d82

File tree

6 files changed

+260
-13
lines changed

6 files changed

+260
-13
lines changed

ecs-cli/modules/cli/local/local_app.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,14 @@ func Up(c *cli.Context) {
4444
}
4545
network.Setup(docker)
4646
}
47+
48+
// Stop stops a running local ECS task.
49+
//
50+
// If the user stops the last running task in the local network then also remove the network.
51+
func Stop(c *cli.Context) {
52+
docker, err := client.NewEnvClient() // Temporary client created to test network.Teardown()
53+
if err != nil {
54+
logrus.Fatal("Could not connect to docker", err)
55+
}
56+
defer network.Teardown(docker)
57+
}

ecs-cli/modules/cli/local/network/setup.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func createLocalNetwork(dockerClient networkCreator) {
112112
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
113113
defer cancel()
114114

115+
logrus.Infof("Creating network: %s...", EcsLocalNetworkName)
115116
resp, err := dockerClient.NetworkCreate(ctx, EcsLocalNetworkName, types.NetworkCreate{
116117
IPAM: &network.IPAM{
117118
Config: []network.IPAMConfig{
@@ -168,7 +169,7 @@ func createLocalEndpointsContainer(dockerClient containerStarter) string {
168169
)
169170
if err != nil {
170171
if strings.Contains(err.Error(), "Conflict") {
171-
// We already created this container before since there is a name conflict, fetch its ID and return it.
172+
// We already created this container before, fetch its ID and return it.
172173
containerID := localEndpointsContainerID(dockerClient)
173174
logrus.Infof("The %s container already exists with ID %s", localEndpointsContainerName, containerID)
174175
return containerID

ecs-cli/modules/cli/local/network/setup_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,13 @@ func TestSetup(t *testing.T) {
9393

9494
for name, tc := range tests {
9595
t.Run(name, func(t *testing.T) {
96-
mockDocker := tc.configureCalls(newMockLocalNetworkStarter(t))
96+
ctrl := gomock.NewController(t)
97+
defer ctrl.Finish()
98+
99+
mockDocker := mock_network.NewMockLocalEndpointsStarter(ctrl)
100+
mockDocker = tc.configureCalls(mockDocker)
97101

98102
Setup(mockDocker)
99103
})
100104
}
101105
}
102-
103-
func newMockLocalNetworkStarter(t *testing.T) *mock_network.MockLocalEndpointsStarter {
104-
ctrl := gomock.NewController(t)
105-
defer ctrl.Finish()
106-
107-
return mock_network.NewMockLocalEndpointsStarter(ctrl)
108-
}

ecs-cli/modules/cli/local/network/teardown.go

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,134 @@
1414
package network
1515

1616
import (
17+
"strings"
1718
"time"
1819

1920
"github.com/docker/docker/api/types"
21+
"github.com/sirupsen/logrus"
2022
"golang.org/x/net/context"
2123
)
2224

23-
// LocalEndpointsStopper groups the Docker NetworkInspect, ContainerStop, and NetworkRemove functions.
25+
// LocalEndpointsStopper groups the Docker NetworkInspect, ContainerStop, ContainerRemove, and NetworkRemove functions.
2426
//
25-
// These functions can be used together to remove a network once unwanted containers in the network are stopped.
27+
// These functions can be used together to remove a network once unwanted containers are stopped.
2628
type LocalEndpointsStopper interface {
29+
networkInspector
30+
containerStopper
31+
containerRemover
32+
networkRemover
33+
}
34+
35+
type networkInspector interface {
2736
NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
37+
}
38+
39+
type containerStopper interface {
2840
ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error
41+
}
42+
43+
type containerRemover interface {
44+
ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error
45+
}
46+
47+
type networkRemover interface {
2948
NetworkRemove(ctx context.Context, networkID string) error
3049
}
3150

32-
// Teardown stops the Local Endpoints container and removes the Local network created by Setup.
51+
// Teardown removes both the Local Endpoints container and the Local network created by Setup.
3352
// If there are other containers running in the network besides the endpoints container, this function does nothing.
3453
//
3554
// If there is any unexpected errors, we exit the program with a fatal log.
36-
func Teardown(dockerClient *LocalEndpointsStopper) {
55+
func Teardown(dockerClient LocalEndpointsStopper) {
56+
if hasRunningTasksInNetwork(dockerClient) {
57+
return
58+
}
59+
logrus.Infof("The network %s has no more running tasks, stopping the endpoints containers...", EcsLocalNetworkName)
60+
61+
stopEndpointsContainer(dockerClient)
62+
removeEndpointsContainer(dockerClient)
63+
removeLocalNetwork(dockerClient)
64+
}
65+
66+
// hasRunningTasksInNetwork returns true if there are other containers besides the
67+
// endpoints container running in the local network, false otherwise.
68+
func hasRunningTasksInNetwork(d networkInspector) bool {
69+
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
70+
defer cancel()
71+
72+
resp, err := d.NetworkInspect(ctx, EcsLocalNetworkName, types.NetworkInspectOptions{})
73+
if err != nil {
74+
logrus.Fatalf("Failed to inspect network %s due to %v", EcsLocalNetworkName, err)
75+
}
76+
77+
if len(resp.Containers) > 1 {
78+
// Has other containers running in the network
79+
return true
80+
}
81+
82+
for _, container := range resp.Containers {
83+
if container.Name != localEndpointsContainerName {
84+
// The only container running in the network is a task without the endpoints container.
85+
// This scenario should not happen unless the user themselves stopped the endpoints container.
86+
logrus.Warnf("The %s container is running in the %s network without the %s container, please stop it first",
87+
container.Name, EcsLocalNetworkName, localEndpointsContainerName)
88+
return true
89+
}
90+
}
91+
92+
return false
93+
}
94+
95+
func stopEndpointsContainer(d containerStopper) {
96+
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
97+
defer cancel()
98+
99+
err := d.ContainerStop(ctx, localEndpointsContainerName, nil)
100+
if err != nil {
101+
if strings.Contains(strings.ToLower(err.Error()), "no such container") {
102+
// The container was removed, do nothing.
103+
return
104+
}
105+
logrus.Fatalf("Failed to stop %s container due to %v", localEndpointsContainerName, err)
106+
}
107+
logrus.Infof("Stopped the %s container successfully, removing it...", localEndpointsContainerName)
108+
}
109+
110+
// removeEndpointsContainer removes the endpoints container.
111+
//
112+
// If we do not remove the container, then the user will receive a "network not found" error on using "local up".
113+
// Here is a sample scenario:
114+
// 1) User runs "local up" and creates a new local network with an endpoints container.
115+
// 2) User runs "local down" and stops the endpoints container but does not remove it, however the network is removed.
116+
// 3) User runs "local up" again and creates a new local network but re-starts the old endpoints container.
117+
// The old endpoints container tries to connect to the network created in step 1) and fails.
118+
func removeEndpointsContainer(d containerRemover) {
119+
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
120+
defer cancel()
121+
122+
err := d.ContainerRemove(ctx, localEndpointsContainerName, types.ContainerRemoveOptions{})
123+
if err != nil {
124+
if strings.Contains(strings.ToLower(err.Error()), "no such container") {
125+
// The container was removed, do nothing.
126+
return
127+
}
128+
logrus.Fatalf("Failed to remove %s container due to %v", localEndpointsContainerName, err)
129+
}
130+
logrus.Infof("Removed the %s container successfully, removing the %s network...",
131+
localEndpointsContainerName, EcsLocalNetworkName)
132+
}
133+
134+
func removeLocalNetwork(d networkRemover) {
135+
ctx, cancel := context.WithTimeout(context.Background(), dockerTimeout)
136+
defer cancel()
37137

138+
err := d.NetworkRemove(ctx, EcsLocalNetworkName)
139+
if err != nil {
140+
if strings.Contains(strings.ToLower(err.Error()), "no such network") {
141+
// The network was removed, do nothing.
142+
return
143+
}
144+
logrus.Fatalf("Failed to remove %s network due to %v", EcsLocalNetworkName, err)
145+
}
146+
logrus.Infof("Removed the %s network successfully", EcsLocalNetworkName)
38147
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package network
15+
16+
import (
17+
"fmt"
18+
"testing"
19+
20+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/network/mock_network"
21+
"github.com/docker/docker/api/types"
22+
"github.com/golang/mock/gomock"
23+
"github.com/pkg/errors"
24+
)
25+
26+
type mockStopperCalls func(mock *mock_network.MockLocalEndpointsStopper) *mock_network.MockLocalEndpointsStopper
27+
28+
func TestTeardown(t *testing.T) {
29+
tests := map[string]struct {
30+
configureCalls mockStopperCalls
31+
}{
32+
"network with no containers": {
33+
configureCalls: func(mock *mock_network.MockLocalEndpointsStopper) *mock_network.MockLocalEndpointsStopper {
34+
gomock.InOrder(
35+
mock.EXPECT().NetworkInspect(gomock.Any(), EcsLocalNetworkName, gomock.Any()).
36+
Return(types.NetworkResource{Containers: make(map[string]types.EndpointResource)}, nil),
37+
mock.EXPECT().ContainerStop(gomock.Any(), localEndpointsContainerName, nil).
38+
Return(errors.New(fmt.Sprintf("No such container: %s", localEndpointsContainerName))),
39+
mock.EXPECT().ContainerRemove(gomock.Any(), localEndpointsContainerName, gomock.Any()).
40+
Return(errors.New(fmt.Sprintf("No such container: %s", localEndpointsContainerName))),
41+
mock.EXPECT().NetworkRemove(gomock.Any(), EcsLocalNetworkName),
42+
)
43+
return mock
44+
},
45+
},
46+
"network with only local endpoints": {
47+
configureCalls: func(mock *mock_network.MockLocalEndpointsStopper) *mock_network.MockLocalEndpointsStopper {
48+
gomock.InOrder(
49+
mock.EXPECT().NetworkInspect(gomock.Any(), gomock.Eq(EcsLocalNetworkName), gomock.Any()).Return(
50+
types.NetworkResource{
51+
Containers: map[string]types.EndpointResource{
52+
localEndpointsContainerName: {
53+
Name: localEndpointsContainerName,
54+
},
55+
},
56+
}, nil),
57+
mock.EXPECT().ContainerStop(gomock.Any(), localEndpointsContainerName, nil),
58+
mock.EXPECT().ContainerRemove(gomock.Any(), localEndpointsContainerName, gomock.Any()),
59+
mock.EXPECT().NetworkRemove(gomock.Any(), EcsLocalNetworkName),
60+
)
61+
return mock
62+
},
63+
},
64+
"network with no running local endpoints": {
65+
configureCalls: func(mock *mock_network.MockLocalEndpointsStopper) *mock_network.MockLocalEndpointsStopper {
66+
gomock.InOrder(
67+
mock.EXPECT().NetworkInspect(gomock.Any(), gomock.Eq(EcsLocalNetworkName), gomock.Any()).Return(
68+
types.NetworkResource{
69+
Containers: map[string]types.EndpointResource{
70+
"some_container": {
71+
Name: "some_container",
72+
},
73+
},
74+
}, nil),
75+
76+
// Should not be invoked
77+
mock.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Times(0),
78+
mock.EXPECT().ContainerRemove(gomock.Any(), gomock.Any(), gomock.Any()).Times(0),
79+
mock.EXPECT().NetworkRemove(gomock.Any(), gomock.Any()).Times(0),
80+
)
81+
return mock
82+
},
83+
},
84+
"network with running tasks": {
85+
configureCalls: func(mock *mock_network.MockLocalEndpointsStopper) *mock_network.MockLocalEndpointsStopper {
86+
gomock.InOrder(
87+
mock.EXPECT().NetworkInspect(gomock.Any(), gomock.Eq(EcsLocalNetworkName), gomock.Any()).Return(
88+
types.NetworkResource{
89+
Containers: map[string]types.EndpointResource{
90+
"some_container": {
91+
Name: "some_container",
92+
},
93+
localEndpointsContainerName: {
94+
Name: localEndpointsContainerName,
95+
},
96+
},
97+
}, nil),
98+
// Should not be invoked
99+
mock.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Times(0),
100+
mock.EXPECT().ContainerRemove(gomock.Any(), gomock.Any(), gomock.Any()).Times(0),
101+
mock.EXPECT().NetworkRemove(gomock.Any(), gomock.Any()).Times(0),
102+
)
103+
return mock
104+
},
105+
},
106+
}
107+
108+
for name, tc := range tests {
109+
t.Run(name, func(t *testing.T) {
110+
ctrl := gomock.NewController(t)
111+
defer ctrl.Finish()
112+
113+
mockDocker := mock_network.NewMockLocalEndpointsStopper(ctrl)
114+
mockDocker = tc.configureCalls(mockDocker)
115+
116+
Teardown(mockDocker)
117+
})
118+
}
119+
}

ecs-cli/modules/commands/local/local_command.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func LocalCommand() cli.Command {
3232
Subcommands: []cli.Command{
3333
createCommand(),
3434
upCommand(),
35+
stopCommand(),
3536
},
3637
}
3738
}
@@ -55,6 +56,15 @@ func upCommand() cli.Command {
5556
}
5657
}
5758

59+
// TODO This is a placeholder function used to test the teardown of the ECS local network.
60+
func stopCommand() cli.Command {
61+
return cli.Command{
62+
Name: "stop",
63+
Usage: "Stop a running local ECS task.",
64+
Action: local.Stop,
65+
}
66+
}
67+
5868
func createFlags() []cli.Flag {
5969
return []cli.Flag{
6070
cli.StringFlag{

0 commit comments

Comments
 (0)