diff --git a/pkg/compose/up.go b/pkg/compose/up.go index b94c01f61..20acfa487 100644 --- a/pkg/compose/up.go +++ b/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, }) }) } diff --git a/pkg/e2e/assert.go b/pkg/e2e/assert.go new file mode 100644 index 000000000..5cfcc1d5d --- /dev/null +++ b/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"], + ) + } +} diff --git a/pkg/e2e/buffer.go b/pkg/e2e/buffer.go new file mode 100644 index 000000000..787e6c358 --- /dev/null +++ b/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) +} diff --git a/pkg/e2e/fixtures/ups-deps-stop/compose.yaml b/pkg/e2e/fixtures/ups-deps-stop/compose.yaml new file mode 100644 index 000000000..c99087f65 --- /dev/null +++ b/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' diff --git a/pkg/e2e/fixtures/ups-deps-stop/orphan.yaml b/pkg/e2e/fixtures/ups-deps-stop/orphan.yaml new file mode 100644 index 000000000..69e50e39c --- /dev/null +++ b/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' diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index f0be91511..c0ba517bf 100644 --- a/pkg/e2e/up_test.go +++ b/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") +}