瀏覽代碼

up: do not stop dependency containers (#9701)

This keeps parity with v1, where only the containers explicitly
passed to `up` are torn down when `Ctrl-C` is hit, so any
dependencies that got launched (or orphan containers hanging
around) should not be touched.

Fixes #9696.

Signed-off-by: Milas Bowman <[email protected]>
Milas Bowman 3 年之前
父節點
當前提交
765c071c89

+ 2 - 2
pkg/compose/up.go

@@ -61,12 +61,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 			go func() {
 				<-signalChan
 				s.Kill(ctx, project.Name, api.KillOptions{ //nolint:errcheck
-					Services: project.ServiceNames(),
+					Services: options.Create.Services,
 				})
 			}()
 
 			return s.Stop(ctx, project.Name, api.StopOptions{
-				Services: project.ServiceNames(),
+				Services: options.Create.Services,
 			})
 		})
 	}

+ 46 - 0
pkg/e2e/assert.go

@@ -0,0 +1,46 @@
+/*
+   Copyright 2022 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 (
+	"encoding/json"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+// RequireServiceState ensures that the container is in the expected state
+// (running or exited).
+func RequireServiceState(t testing.TB, cli *CLI, service string, state string) {
+	t.Helper()
+	psRes := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
+	var psOut []map[string]interface{}
+	require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &psOut),
+		"Invalid `compose ps` JSON output")
+
+	for _, svc := range psOut {
+		require.Equal(t, service, svc["Service"],
+			"Found ps output for unexpected service")
+		require.Equalf(t,
+			strings.ToLower(state),
+			strings.ToLower(svc["State"].(string)),
+			"Service %q (%s) not in expected state",
+			service, svc["Name"],
+		)
+	}
+}

+ 66 - 0
pkg/e2e/buffer.go

@@ -0,0 +1,66 @@
+/*
+   Copyright 2022 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 (
+	"bytes"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+type lockedBuffer struct {
+	mu  sync.Mutex
+	buf bytes.Buffer
+}
+
+func (l *lockedBuffer) Read(p []byte) (n int, err error) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	return l.buf.Read(p)
+}
+
+func (l *lockedBuffer) Write(p []byte) (n int, err error) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	return l.buf.Write(p)
+}
+
+func (l *lockedBuffer) String() string {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	return l.buf.String()
+}
+
+func (l *lockedBuffer) RequireEventuallyContains(t testing.TB, v string) {
+	t.Helper()
+	var bufContents strings.Builder
+	require.Eventuallyf(t, func() bool {
+		l.mu.Lock()
+		defer l.mu.Unlock()
+		if _, err := l.buf.WriteTo(&bufContents); err != nil {
+			require.FailNowf(t, "Failed to copy from buffer",
+				"Error: %v", err)
+		}
+		return strings.Contains(bufContents.String(), v)
+	}, 2*time.Second, 20*time.Millisecond,
+		"Buffer did not contain %q\n============\n%s\n============",
+		v, &bufContents)
+}

+ 11 - 0
pkg/e2e/fixtures/ups-deps-stop/compose.yaml

@@ -0,0 +1,11 @@
+services:
+  dependency:
+    image: alpine
+    init: true
+    command: /bin/sh -c 'while true; do echo "hello dependency"; sleep 1; done'
+
+  app:
+    depends_on: ['dependency']
+    image: alpine
+    init: true
+    command: /bin/sh -c 'while true; do echo "hello app"; sleep 1; done'

+ 5 - 0
pkg/e2e/fixtures/ups-deps-stop/orphan.yaml

@@ -0,0 +1,5 @@
+services:
+  orphan:
+    image: alpine
+    init: true
+    command: /bin/sh -c 'while true; do echo "hello orphan"; sleep 1; done'

+ 73 - 1
pkg/e2e/up_test.go

@@ -1,5 +1,5 @@
 /*
-   Copyright 2020 Docker Compose CLI authors
+   Copyright 2022 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.
@@ -17,8 +17,14 @@
 package e2e
 
 import (
+	"context"
+	"os/exec"
+	"syscall"
 	"testing"
+	"time"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"gotest.tools/v3/icmd"
 )
 
@@ -31,3 +37,69 @@ func TestUpServiceUnhealthy(t *testing.T) {
 
 	c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
 }
+
+func TestUpDependenciesNotStopped(t *testing.T) {
+	c := NewParallelCLI(t, WithEnv(
+		"COMPOSE_PROJECT_NAME=up-deps-stop",
+	))
+
+	reset := func() {
+		c.RunDockerComposeCmdNoCheck(t, "down", "-t=0", "--remove-orphans", "-v")
+	}
+	reset()
+	t.Cleanup(reset)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	t.Log("Launching orphan container (background)")
+	c.RunDockerComposeCmd(t,
+		"-f=./fixtures/ups-deps-stop/orphan.yaml",
+		"up",
+		"--wait",
+		"--detach",
+		"orphan",
+	)
+	RequireServiceState(t, c, "orphan", "running")
+
+	t.Log("Launching app container with implicit dependency")
+	var upOut lockedBuffer
+	var upCmd *exec.Cmd
+	go func() {
+		testCmd := c.NewDockerComposeCmd(t,
+			"-f=./fixtures/ups-deps-stop/compose.yaml",
+			"up",
+			"app",
+		)
+		cmd := exec.CommandContext(ctx, testCmd.Command[0], testCmd.Command[1:]...)
+		cmd.Env = testCmd.Env
+		cmd.Stdout = &upOut
+		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+
+		assert.NoError(t, cmd.Start(), "Failed to run compose up")
+		upCmd = cmd
+	}()
+
+	t.Log("Waiting for containers to be in running state")
+	upOut.RequireEventuallyContains(t, "hello app")
+	RequireServiceState(t, c, "app", "running")
+	RequireServiceState(t, c, "dependency", "running")
+
+	t.Log("Simulating Ctrl-C")
+	require.NoError(t, syscall.Kill(-upCmd.Process.Pid, syscall.SIGINT),
+		"Failed to send SIGINT to compose up process")
+
+	time.AfterFunc(5*time.Second, cancel)
+
+	t.Log("Waiting for `compose up` to exit")
+	err := upCmd.Wait()
+	if err != nil {
+		exitErr := err.(*exec.ExitError)
+		require.EqualValues(t, exitErr.ExitCode(), 130)
+	}
+
+	RequireServiceState(t, c, "app", "exited")
+	// dependency should still be running
+	RequireServiceState(t, c, "dependency", "running")
+	RequireServiceState(t, c, "orphan", "running")
+}