mirror of https://github.com/docker/compose.git
Handle Ctrl+C for compose CLI plugin.
Could do something nicer passing the context to the compose command, rather than intercepting it and checking if it’s “.WithCancel” or not... Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>
This commit is contained in:
parent
0785114b90
commit
de3fa40bae
|
@ -20,7 +20,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
|
@ -32,6 +34,7 @@ import (
|
|||
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
"github.com/docker/compose-cli/api/context/store"
|
||||
"github.com/docker/compose-cli/api/errdefs"
|
||||
"github.com/docker/compose-cli/cli/formatter"
|
||||
"github.com/docker/compose-cli/cli/metrics"
|
||||
)
|
||||
|
@ -42,8 +45,26 @@ type Command func(context.Context, []string) error
|
|||
//Adapt a Command func to cobra library
|
||||
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
err := fn(cmd.Context(), args)
|
||||
ctx := cmd.Context()
|
||||
contextString := fmt.Sprintf("%s", ctx)
|
||||
if !strings.HasSuffix(contextString, ".WithCancel") { // need to handle cancel
|
||||
cancellableCtx, cancel := context.WithCancel(cmd.Context())
|
||||
ctx = cancellableCtx
|
||||
s := make(chan os.Signal, 1)
|
||||
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-s
|
||||
cancel()
|
||||
}()
|
||||
}
|
||||
err := fn(ctx, args)
|
||||
var composeErr metrics.ComposeError
|
||||
if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
|
||||
err = dockercli.StatusError{
|
||||
StatusCode: 130,
|
||||
Status: metrics.CanceledStatus,
|
||||
}
|
||||
}
|
||||
if errors.As(err, &composeErr) {
|
||||
err = dockercli.StatusError{
|
||||
StatusCode: composeErr.GetMetricsFailureCategory().ExitCode,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
services:
|
||||
service1:
|
||||
build: service1
|
|
@ -0,0 +1,17 @@
|
|||
# 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.
|
||||
|
||||
FROM busybox
|
||||
|
||||
RUN sleep infinity
|
|
@ -17,7 +17,11 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -84,3 +88,69 @@ func TestComposeMetrics(t *testing.T) {
|
|||
}, usage)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeCancel(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
s := NewMetricsServer(c.MetricsSocket())
|
||||
s.Start()
|
||||
defer s.Stop()
|
||||
|
||||
started := false
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
c.RunDockerCmd("help", "ps")
|
||||
if len(s.GetUsage()) > 0 {
|
||||
started = true
|
||||
fmt.Printf(" [%s] Server up in %d ms\n", t.Name(), i*100)
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
assert.Assert(t, started, "Metrics mock server not available after 3 secs")
|
||||
|
||||
t.Run("metrics on cancel Compose build", func(t *testing.T) {
|
||||
s.ResetUsage()
|
||||
|
||||
c.RunDockerCmd("compose", "ls")
|
||||
buildProjectPath := "../compose/fixtures/build-infinite/docker-compose.yml"
|
||||
|
||||
// require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal.
|
||||
// sending kill signal
|
||||
cmd, stdout, stderr, err := StartWithNewGroupID(c.NewDockerCmd("compose", "-f", buildProjectPath, "build", "--progress", "plain"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
c.WaitForCondition(func() (bool, string) {
|
||||
out := stdout.String()
|
||||
errors := stderr.String()
|
||||
return strings.Contains(out, "RUN sleep infinity"), fmt.Sprintf("'RUN sleep infinity' not found in : \n%s\nStderr: \n%s\n", out, errors)
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
|
||||
err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default
|
||||
|
||||
assert.NilError(t, err)
|
||||
c.WaitForCondition(func() (bool, string) {
|
||||
out := stdout.String()
|
||||
errors := stderr.String()
|
||||
return strings.Contains(out, "CANCELED"), fmt.Sprintf("'CANCELED' not found in : \n%s\nStderr: \n%s\n", out, errors)
|
||||
}, 10*time.Second, 1*time.Second)
|
||||
|
||||
usage := s.GetUsage()
|
||||
assert.DeepEqual(t, []string{
|
||||
`{"command":"compose ls","context":"moby","source":"cli","status":"success"}`,
|
||||
`{"command":"compose build","context":"moby","source":"cli","status":"canceled"}`,
|
||||
}, usage)
|
||||
})
|
||||
}
|
||||
|
||||
func StartWithNewGroupID(command icmd.Cmd) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer, error) {
|
||||
cmd := exec.Command(command.Command[0], command.Command[1:]...)
|
||||
cmd.Env = command.Env
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Start()
|
||||
return cmd, &stdout, &stderr, err
|
||||
}
|
||||
|
|
|
@ -252,6 +252,18 @@ func (c *E2eCLI) WaitForCmdResult(command icmd.Cmd, predicate func(*icmd.Result)
|
|||
poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
|
||||
}
|
||||
|
||||
// WaitForCondition wait for predicate to execute to true
|
||||
func (c *E2eCLI) WaitForCondition(predicate func() (bool, string), timeout time.Duration, delay time.Duration) {
|
||||
checkStopped := func(logt poll.LogT) poll.Result {
|
||||
pass, description := predicate()
|
||||
if !pass {
|
||||
return poll.Continue("Condition not met: %q", description)
|
||||
}
|
||||
return poll.Success()
|
||||
}
|
||||
poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
|
||||
}
|
||||
|
||||
// PathEnvVar returns path (os sensitive) for running test
|
||||
func (c *E2eCLI) PathEnvVar() string {
|
||||
path := c.BinDir + ":" + os.Getenv("PATH")
|
||||
|
|
Loading…
Reference in New Issue