mirror of
https://github.com/docker/compose.git
synced 2025-07-01 02:44:25 +02:00
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/cli"
|
"github.com/compose-spec/compose-go/cli"
|
||||||
"github.com/compose-spec/compose-go/types"
|
"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/compose"
|
||||||
"github.com/docker/compose-cli/api/context/store"
|
"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/formatter"
|
||||||
"github.com/docker/compose-cli/cli/metrics"
|
"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
|
//Adapt a Command func to cobra library
|
||||||
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
|
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
|
||||||
return 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
|
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) {
|
if errors.As(err, &composeErr) {
|
||||||
err = dockercli.StatusError{
|
err = dockercli.StatusError{
|
||||||
StatusCode: composeErr.GetMetricsFailureCategory().ExitCode,
|
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
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -84,3 +88,69 @@ func TestComposeMetrics(t *testing.T) {
|
|||||||
}, usage)
|
}, 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))
|
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
|
// PathEnvVar returns path (os sensitive) for running test
|
||||||
func (c *E2eCLI) PathEnvVar() string {
|
func (c *E2eCLI) PathEnvVar() string {
|
||||||
path := c.BinDir + ":" + os.Getenv("PATH")
|
path := c.BinDir + ":" + os.Getenv("PATH")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user