docker compose exec to return command exit code

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-04-28 17:59:37 +02:00
parent 38b4220bdb
commit 15eab93b31
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
11 changed files with 95 additions and 32 deletions

View File

@ -227,8 +227,8 @@ func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project,
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { func (cs *aciComposeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented

View File

@ -92,8 +92,8 @@ func (c *composeService) Remove(ctx context.Context, project *types.Project, opt
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
func (c *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { func (c *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (c *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error { func (c *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error {

View File

@ -61,7 +61,7 @@ type Service interface {
// Remove executes the equivalent to a `compose rm` // Remove executes the equivalent to a `compose rm`
Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error)
// Exec executes a command in a running service container // 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 executes the equivalent to a `compose pause`
Pause(ctx context.Context, project string, options PauseOptions) error Pause(ctx context.Context, project string, options PauseOptions) error
// UnPause executes the equivalent to a `compose unpause` // UnPause executes the equivalent to a `compose unpause`

View File

@ -108,7 +108,7 @@ func (s *ServiceDelegator) Remove(ctx context.Context, project *types.Project, o
} }
//Exec implements Service interface //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) return s.Delegate.Exec(ctx, project, options)
} }

View File

@ -108,8 +108,8 @@ func (s NoImpl) Remove(ctx context.Context, project *types.Project, options Remo
} }
//Exec implements Service interface //Exec implements Service interface
func (s NoImpl) Exec(ctx context.Context, project *types.Project, options RunOptions) error { func (s NoImpl) Exec(ctx context.Context, project *types.Project, opts RunOptions) (int, error) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
//Pause implements Service interface //Pause implements Service interface

View File

@ -22,6 +22,7 @@ import (
"os" "os"
"github.com/containerd/console" "github.com/containerd/console"
"github.com/docker/cli/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/compose" "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.Writer = con
execOpts.Reader = 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
} }

View File

@ -25,6 +25,6 @@ import (
"github.com/docker/compose-cli/api/errdefs" "github.com/docker/compose-cli/api/errdefs"
) )
func (b *ecsAPIService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { func (b *ecsAPIService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }

View File

@ -184,8 +184,8 @@ func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project,
return e.compose.Remove(ctx, project, options) return e.compose.Remove(ctx, project, options)
} }
func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (e ecsLocalSimulation) Pause(ctx context.Context, project string, options compose.PauseOptions) error { func (e ecsLocalSimulation) Pause(ctx context.Context, project string, options compose.PauseOptions) error {

View File

@ -248,8 +248,8 @@ func (s *composeService) Remove(ctx context.Context, project *types.Project, opt
} }
// Exec executes a command in a running service container // Exec executes a command in a running service container
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) {
return errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (s *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error { func (s *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error {

View File

@ -28,10 +28,10 @@ import (
"github.com/docker/compose-cli/api/compose" "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) service, err := project.GetService(opts.Service)
if err != nil { if err != nil {
return err return 0, err
} }
containers, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{ 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 { if err != nil {
return err return 0, err
} }
if len(containers) < 1 { 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] container := containers[0]
@ -63,11 +63,11 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
AttachStderr: true, AttachStderr: true,
}) })
if err != nil { if err != nil {
return err return 0, err
} }
if opts.Detach { if opts.Detach {
return s.apiClient.ContainerExecStart(ctx, exec.ID, apitypes.ExecStartCheck{ return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, apitypes.ExecStartCheck{
Detach: true, Detach: true,
Tty: opts.Tty, Tty: opts.Tty,
}) })
@ -78,19 +78,19 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
Tty: opts.Tty, Tty: opts.Tty,
}) })
if err != nil { if err != nil {
return err return 0, err
} }
defer resp.Close() defer resp.Close()
if opts.Tty { if opts.Tty {
s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize) s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize)
if err != nil { if err != nil {
return err return 0, err
} }
} }
readChannel := make(chan error, 10) readChannel := make(chan error)
writeChannel := make(chan error, 10) writeChannel := make(chan error)
go func() { go func() {
_, err := io.Copy(opts.Writer, resp.Reader) _, err := io.Copy(opts.Writer, resp.Reader)
@ -102,12 +102,23 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
writeChannel <- err writeChannel <- err
}() }()
for { select {
select { case err = <-readChannel:
case err := <-readChannel: break
return err case err = <-writeChannel:
case err := <-writeChannel: break
return err
}
} }
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
} }

View File

@ -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})
})
}