Skip to content

Commit

Permalink
up: do not stop dependency containers (#9701)
Browse files Browse the repository at this point in the history
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 <milas.bowman@docker.com>
  • Loading branch information
milas committed Aug 2, 2022
1 parent 3dfdad6 commit 765c071
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 3 deletions.
4 changes: 2 additions & 2 deletions pkg/compose/up.go
Expand Up @@ -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,
})
})
}
Expand Down
46 changes: 46 additions & 0 deletions 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 changes: 66 additions & 0 deletions 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 changes: 11 additions & 0 deletions 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 changes: 5 additions & 0 deletions 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'
74 changes: 73 additions & 1 deletion 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.
Expand All @@ -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"
)

Expand All @@ -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")
}

0 comments on commit 765c071

Please sign in to comment.