diff --git a/aci/compose.go b/aci/compose.go index 62f9e1cc3..22300c291 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -227,8 +227,8 @@ func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, return nil, errdefs.ErrNotImplemented } -func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { - return errdefs.ErrNotImplemented +func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } func (cs *aciComposeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { return nil, errdefs.ErrNotImplemented diff --git a/api/client/compose.go b/api/client/compose.go index dea38e1ed..77a7e34f6 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -92,8 +92,8 @@ func (c *composeService) Remove(ctx context.Context, project *types.Project, opt return nil, errdefs.ErrNotImplemented } -func (c *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { - return errdefs.ErrNotImplemented +func (c *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } func (c *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error { diff --git a/api/compose/api.go b/api/compose/api.go index 066d39b9e..18873d7f5 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -61,7 +61,7 @@ type Service interface { // Remove executes the equivalent to a `compose rm` Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) // Exec executes a command in a running service container - Exec(ctx context.Context, project *types.Project, opts RunOptions) error + Exec(ctx context.Context, project *types.Project, opts RunOptions) (int, error) // Pause executes the equivalent to a `compose pause` Pause(ctx context.Context, project string, options PauseOptions) error // UnPause executes the equivalent to a `compose unpause` diff --git a/api/compose/delegator.go b/api/compose/delegator.go index c79ded64f..bab938dbd 100644 --- a/api/compose/delegator.go +++ b/api/compose/delegator.go @@ -108,7 +108,7 @@ func (s *ServiceDelegator) Remove(ctx context.Context, project *types.Project, o } //Exec implements Service interface -func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, options RunOptions) error { +func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, options RunOptions) (int, error) { return s.Delegate.Exec(ctx, project, options) } diff --git a/api/compose/noimpl.go b/api/compose/noimpl.go index 172af3e47..ec7963ef3 100644 --- a/api/compose/noimpl.go +++ b/api/compose/noimpl.go @@ -108,8 +108,8 @@ func (s NoImpl) Remove(ctx context.Context, project *types.Project, options Remo } //Exec implements Service interface -func (s NoImpl) Exec(ctx context.Context, project *types.Project, options RunOptions) error { - return errdefs.ErrNotImplemented +func (s NoImpl) Exec(ctx context.Context, project *types.Project, opts RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } //Pause implements Service interface diff --git a/cli/cmd/compose/exec.go b/cli/cmd/compose/exec.go index c2a5b908d..d583e0e5f 100644 --- a/cli/cmd/compose/exec.go +++ b/cli/cmd/compose/exec.go @@ -22,6 +22,7 @@ import ( "os" "github.com/containerd/console" + "github.com/docker/cli/cli" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/compose" @@ -108,5 +109,13 @@ func runExec(ctx context.Context, backend compose.Service, opts execOpts) error execOpts.Writer = con execOpts.Reader = con } - return backend.Exec(ctx, project, execOpts) + exitCode, err := backend.Exec(ctx, project, execOpts) + if exitCode != 0 { + errMsg := "" + if err != nil { + errMsg = err.Error() + } + return cli.StatusError{StatusCode: exitCode, Status: errMsg} + } + return err } diff --git a/ecs/exec.go b/ecs/exec.go index 44f9c2e14..829ed3ed3 100644 --- a/ecs/exec.go +++ b/ecs/exec.go @@ -25,6 +25,6 @@ import ( "github.com/docker/compose-cli/api/errdefs" ) -func (b *ecsAPIService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { - return errdefs.ErrNotImplemented +func (b *ecsAPIService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } diff --git a/ecs/local/compose.go b/ecs/local/compose.go index aca732ffc..214843138 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -184,8 +184,8 @@ func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project, return e.compose.Remove(ctx, project, options) } -func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { - return errdefs.ErrNotImplemented +func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } func (e ecsLocalSimulation) Pause(ctx context.Context, project string, options compose.PauseOptions) error { diff --git a/kube/compose.go b/kube/compose.go index e72937fdc..9fb4e973b 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -248,8 +248,8 @@ func (s *composeService) Remove(ctx context.Context, project *types.Project, opt } // Exec executes a command in a running service container -func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { - return errdefs.ErrNotImplemented +func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { + return 0, errdefs.ErrNotImplemented } func (s *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error { diff --git a/local/compose/exec.go b/local/compose/exec.go index 90fcba37e..57996e106 100644 --- a/local/compose/exec.go +++ b/local/compose/exec.go @@ -28,10 +28,10 @@ import ( "github.com/docker/compose-cli/api/compose" ) -func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { +func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { service, err := project.GetService(opts.Service) if err != nil { - return err + return 0, err } containers, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{ @@ -42,10 +42,10 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts ), }) if err != nil { - return err + return 0, err } if len(containers) < 1 { - return fmt.Errorf("container %s not running", getContainerName(project.Name, service, opts.Index)) + return 0, fmt.Errorf("container %s not running", getContainerName(project.Name, service, opts.Index)) } container := containers[0] @@ -63,11 +63,11 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts AttachStderr: true, }) if err != nil { - return err + return 0, err } if opts.Detach { - return s.apiClient.ContainerExecStart(ctx, exec.ID, apitypes.ExecStartCheck{ + return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, apitypes.ExecStartCheck{ Detach: true, Tty: opts.Tty, }) @@ -78,19 +78,19 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts Tty: opts.Tty, }) if err != nil { - return err + return 0, err } defer resp.Close() if opts.Tty { s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize) if err != nil { - return err + return 0, err } } - readChannel := make(chan error, 10) - writeChannel := make(chan error, 10) + readChannel := make(chan error) + writeChannel := make(chan error) go func() { _, err := io.Copy(opts.Writer, resp.Reader) @@ -102,12 +102,23 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts writeChannel <- err }() - for { - select { - case err := <-readChannel: - return err - case err := <-writeChannel: - return err - } + select { + case err = <-readChannel: + break + case err = <-writeChannel: + break } + + if err != nil { + return 0, err + } + return s.getExecExitStatus(ctx, exec.ID) +} + +func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) { + resp, err := s.apiClient.ContainerExecInspect(ctx, execID) + if err != nil { + return 0, err + } + return resp.ExitCode, nil } diff --git a/local/e2e/compose/compose_exec_test.go b/local/e2e/compose/compose_exec_test.go new file mode 100644 index 000000000..e25810bf3 --- /dev/null +++ b/local/e2e/compose/compose_exec_test.go @@ -0,0 +1,43 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 e2e + +import ( + "testing" + + "gotest.tools/v3/icmd" + + . "github.com/docker/compose-cli/utils/e2e" +) + +func TestLocalComposeExec(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + + const projectName = "compose-e2e-exec" + + c.RunDockerCmd("compose", "--project-directory", "fixtures/simple-composefile", "--project-name", projectName, "up", "-d") + + t.Run("exec true", func(t *testing.T) { + res := c.RunDockerOrExitError("exec", "compose-e2e-exec_simple_1", "/bin/true") + res.Assert(t, icmd.Expected{ExitCode: 0}) + }) + + t.Run("exec false", func(t *testing.T) { + res := c.RunDockerOrExitError("exec", "compose-e2e-exec_simple_1", "/bin/false") + res.Assert(t, icmd.Expected{ExitCode: 1}) + }) +}