diff --git a/aci/aci.go b/aci/aci.go index eca480a6a..f1d1e76dd 100644 --- a/aci/aci.go +++ b/aci/aci.go @@ -24,6 +24,8 @@ import ( "strings" "time" + "github.com/docker/api/errdefs" + "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" @@ -139,6 +141,19 @@ func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, c return containerGroupsClient.Delete(ctx, aciContext.ResourceGroup, containerGroupName) } +func stopACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) error { + containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID) + if err != nil { + return fmt.Errorf("cannot get container group client: %v", err) + } + + result, err := containerGroupsClient.Stop(ctx, aciContext.ResourceGroup, containerGroupName) + if result.StatusCode == http.StatusNotFound { + return errdefs.ErrNotFound + } + return err +} + func execACIContainer(ctx context.Context, aciContext store.AciContext, command, containerGroup string, containerName string) (c containerinstance.ContainerExecResponse, err error) { containerClient, err := getContainerClient(aciContext.SubscriptionID) if err != nil { diff --git a/aci/backend.go b/aci/backend.go index 788da4c3e..08de31886 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -221,8 +221,16 @@ func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) { groupDefinition.Tags[tagName] = to.StringPtr(tagName) } -func (cs *aciContainerService) Stop(ctx context.Context, containerName string, timeout *uint32) error { - return errdefs.ErrNotImplemented +func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error { + if timeout != nil && *timeout != uint32(0) { + return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.") + } + groupName, containerName := getGroupAndContainerName(containerID) + if groupName != containerID { + msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s" + return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName)) + } + return stopACIContainerGroup(ctx, cs.ctx, groupName) } func getGroupAndContainerName(containerID string) (string, string) { diff --git a/cli/cmd/stop.go b/cli/cmd/stop.go new file mode 100644 index 000000000..18fcfe602 --- /dev/null +++ b/cli/cmd/stop.go @@ -0,0 +1,77 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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 cmd + +import ( + "context" + "fmt" + + "github.com/docker/api/errdefs" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/hashicorp/go-multierror" + + "github.com/docker/api/client" +) + +type stopOpts struct { + timeout uint32 +} + +// StopCommand deletes containers +func StopCommand() *cobra.Command { + var opts stopOpts + cmd := &cobra.Command{ + Use: "stop", + Short: "Stop one or more running containers", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runStop(cmd.Context(), args, opts) + }, + } + + cmd.Flags().Uint32Var(&opts.timeout, "timeout", 0, "Seconds to wait for stop before killing it (default 0, no timeout)") + + return cmd +} + +func runStop(ctx context.Context, args []string, opts stopOpts) error { + c, err := client.New(ctx) + if err != nil { + return errors.Wrap(err, "cannot connect to backend") + } + + var errs *multierror.Error + for _, id := range args { + err := c.ContainerService().Stop(ctx, id, &opts.timeout) + if err != nil { + if errdefs.IsNotFoundError(err) { + errs = multierror.Append(errs, fmt.Errorf("container %s not found", id)) + } else { + errs = multierror.Append(errs, err) + } + continue + } + fmt.Println(id) + } + if errs != nil { + errs.ErrorFormat = formatErrors + } + return errs.ErrorOrNil() +} diff --git a/cli/main.go b/cli/main.go index 24e5f70cc..1216bff87 100644 --- a/cli/main.go +++ b/cli/main.go @@ -124,6 +124,7 @@ func main() { login.Command(), logout.Command(), cmd.VersionCommand(version), + cmd.StopCommand(), ) helpFunc := root.HelpFunc() diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index b835f64a5..b3a8baa95 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/tests/aci-e2e/e2e-aci_test.go @@ -360,8 +360,21 @@ func TestContainerRunAttached(t *testing.T) { poll.WaitOn(t, checkLog, poll.WithDelay(1*time.Second), poll.WithTimeout(20*time.Second)) }) - t.Run("rm attached", func(t *testing.T) { - res := c.RunDockerCmd("rm", "-f", container) + t.Run("stop wrong container", func(t *testing.T) { + res := c.RunDockerCmd("stop", "unknown-container") + res.Assert(t, icmd.Expected{ + Err: "Error: container unknown-container not found", + ExitCode: 1, + }) + }) + + t.Run("stop container", func(t *testing.T) { + res := c.RunDockerCmd("stop", container) + res.Assert(t, icmd.Expected{Out: container}) + }) + + t.Run("rm stopped container", func(t *testing.T) { + res := c.RunDockerCmd("rm", container) res.Assert(t, icmd.Expected{Out: container}) }) }