Ver Fonte

docker compose exec to return command exit code

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof há 4 anos atrás
pai
commit
15eab93b31

+ 2 - 2
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

+ 2 - 2
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 {

+ 1 - 1
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`

+ 1 - 1
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)
 }
 

+ 2 - 2
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

+ 10 - 1
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
 }

+ 2 - 2
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
 }

+ 2 - 2
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 {

+ 2 - 2
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 {

+ 28 - 17
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
 }

+ 43 - 0
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})
+	})
+}