diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index e4e3ad188..6fb6f3e3c 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -127,6 +127,24 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project, } sort.Slice(containers, func(i, j int) bool { + // select obsolete containers first, so they get removed as we scale down + if obsolete, _ := mustRecreate(service, containers[i], recreate); obsolete { + // i is obsolete, so must be first in the list + return true + } + if obsolete, _ := mustRecreate(service, containers[j], recreate); obsolete { + // j is obsolete, so must be first in the list + return false + } + + // For up-to-date containers, sort by container number to preserve low-values in container numbers + ni, erri := strconv.Atoi(containers[i].Labels[api.ContainerNumberLabel]) + nj, errj := strconv.Atoi(containers[j].Labels[api.ContainerNumberLabel]) + if erri == nil && errj == nil { + return ni < nj + } + + // If we don't get a container number (?) just sort by creation date return containers[i].Created < containers[j].Created }) for i, container := range containers { diff --git a/pkg/e2e/fixtures/scale/compose.yaml b/pkg/e2e/fixtures/scale/compose.yaml index 9ff67af69..619630876 100644 --- a/pkg/e2e/fixtures/scale/compose.yaml +++ b/pkg/e2e/fixtures/scale/compose.yaml @@ -5,6 +5,8 @@ services: - db db: image: nginx:alpine + environment: + - MAYBE front: image: nginx:alpine deploy: diff --git a/pkg/e2e/scale_test.go b/pkg/e2e/scale_test.go index 1cd80ad1d..21595dd47 100644 --- a/pkg/e2e/scale_test.go +++ b/pkg/e2e/scale_test.go @@ -95,6 +95,78 @@ func TestScaleWithDepsCases(t *testing.T) { checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 1) } +func TestScaleUpAndDownPreserveContainerNumber(t *testing.T) { + const projectName = "scale-up-down-test" + + c := NewCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME="+projectName)) + + reset := func() { + c.RunDockerComposeCmd(t, "down", "--rmi", "all") + } + t.Cleanup(reset) + res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") + + t.Log("scale down removes replica #2") + res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") + + t.Log("scale up restores replica #2") + res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") +} + +func TestScaleDownRemovesObsolete(t *testing.T) { + const projectName = "scale-down-obsolete-test" + c := NewCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME="+projectName)) + + reset := func() { + c.RunDockerComposeCmd(t, "down", "--rmi", "all") + } + t.Cleanup(reset) + res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") + + cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "MAYBE=value") + }) + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") + + t.Log("scale down removes obsolete replica #1") + cmd = c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db") + res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "MAYBE=value") + }) + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") +} + func checkServiceContainer(t *testing.T, stdout, containerName, containerState string, count int) { found := 0 lines := strings.Split(stdout, "\n")