Merge pull request #35851 from tonistiigi/test-names

integration-cli: clarify multi-stage tests names
diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go
index dabab3b..fd95420 100644
--- a/api/server/router/image/image_routes.go
+++ b/api/server/router/image/image_routes.go
@@ -13,7 +13,6 @@
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
-	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/pkg/ioutils"
@@ -46,9 +45,6 @@
 	if err != nil && err != io.EOF { //Do not fail if body is empty.
 		return err
 	}
-	if c == nil {
-		c = &container.Config{}
-	}
 
 	commitCfg := &backend.ContainerCommitConfig{
 		ContainerCommitConfig: types.ContainerCommitConfig{
diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go
index b62d6fc..20f1650 100644
--- a/builder/dockerfile/builder.go
+++ b/builder/dockerfile/builder.go
@@ -396,7 +396,8 @@
 	}
 
 	dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, newBuildArgs(b.options.BuildArgs), newStagesBuildResults())
-	dispatchRequest.state.runConfig = config
+	// We make mutations to the configuration, ensure we have a copy
+	dispatchRequest.state.runConfig = copyRunConfig(config)
 	dispatchRequest.state.imageID = config.Image
 	for _, cmd := range commands {
 		err := dispatch(dispatchRequest, cmd)
diff --git a/daemon/commit.go b/daemon/commit.go
index 0053132..1bdbd6b 100644
--- a/daemon/commit.go
+++ b/daemon/commit.go
@@ -149,6 +149,10 @@
 		defer daemon.containerUnpause(container)
 	}
 
+	if c.MergeConfigs && c.Config == nil {
+		c.Config = container.Config
+	}
+
 	newConfig, err := dockerfile.BuildFromConfig(c.Config, c.Changes)
 	if err != nil {
 		return "", err
diff --git a/daemon/health.go b/daemon/health.go
index f40c0dd..9acf190 100644
--- a/daemon/health.go
+++ b/daemon/health.go
@@ -80,6 +80,7 @@
 	execConfig.Tty = false
 	execConfig.Privileged = false
 	execConfig.User = cntr.Config.User
+	execConfig.WorkingDir = cntr.Config.WorkingDir
 
 	linkedEnv, err := d.setupLinkedContainers(cntr)
 	if err != nil {
diff --git a/integration-cli/docker_cli_commit_test.go b/integration-cli/docker_cli_commit_test.go
index 58a50ce..057c2d6 100644
--- a/integration-cli/docker_cli_commit_test.go
+++ b/integration-cli/docker_cli_commit_test.go
@@ -121,11 +121,19 @@
 		"test", "test-commit")
 	imageID = strings.TrimSpace(imageID)
 
+	// The ordering here is due to `PATH` being overridden from the container's
+	// ENV.  On windows, the container doesn't have a `PATH` ENV variable so
+	// the ordering is the same as the cli.
+	expectedEnv := "[PATH=/foo DEBUG=true test=1]"
+	if testEnv.DaemonPlatform() == "windows" {
+		expectedEnv = "[DEBUG=true test=1 PATH=/foo]"
+	}
+
 	prefix, slash := getPrefixAndSlashFromDaemonPlatform()
 	prefix = strings.ToUpper(prefix) // Force C: as that's how WORKDIR is normalized on Windows
 	expected := map[string]string{
 		"Config.ExposedPorts": "map[8080/tcp:{}]",
-		"Config.Env":          "[DEBUG=true test=1 PATH=/foo]",
+		"Config.Env":          expectedEnv,
 		"Config.Labels":       "map[foo:bar]",
 		"Config.Cmd":          "[/bin/sh]",
 		"Config.WorkingDir":   prefix + slash + "opt",
diff --git a/integration/container/health_test.go b/integration/container/health_test.go
new file mode 100644
index 0000000..8ed86a8
--- /dev/null
+++ b/integration/container/health_test.go
@@ -0,0 +1,61 @@
+package container
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/api/types/strslice"
+	"github.com/docker/docker/client"
+	"github.com/docker/docker/integration/util/request"
+	"github.com/gotestyourself/gotestyourself/poll"
+	"github.com/stretchr/testify/require"
+)
+
+// TestHealthCheckWorkdir verifies that health-checks inherit the containers'
+// working-dir.
+func TestHealthCheckWorkdir(t *testing.T) {
+	defer setupTest(t)()
+	ctx := context.Background()
+	client := request.NewAPIClient(t)
+
+	c, err := client.ContainerCreate(ctx,
+		&container.Config{
+			Image:      "busybox",
+			Tty:        true,
+			WorkingDir: "/foo",
+			Cmd:        strslice.StrSlice([]string{"top"}),
+			Healthcheck: &container.HealthConfig{
+				Test:     []string{"CMD-SHELL", "if [ \"$PWD\" = \"/foo\" ]; then exit 0; else exit 1; fi;"},
+				Interval: 50 * time.Millisecond,
+				Retries:  3,
+			},
+		},
+		&container.HostConfig{},
+		&network.NetworkingConfig{},
+		"healthtest",
+	)
+	require.NoError(t, err)
+	err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{})
+	require.NoError(t, err)
+
+	poll.WaitOn(t, pollForHealthStatus(ctx, client, c.ID, types.Healthy), poll.WithDelay(100*time.Millisecond))
+}
+
+func pollForHealthStatus(ctx context.Context, client client.APIClient, containerID string, healthStatus string) func(log poll.LogT) poll.Result {
+	return func(log poll.LogT) poll.Result {
+		inspect, err := client.ContainerInspect(ctx, containerID)
+
+		switch {
+		case err != nil:
+			return poll.Error(err)
+		case inspect.State.Health.Status == healthStatus:
+			return poll.Success()
+		default:
+			return poll.Continue("waiting for container to become %s", healthStatus)
+		}
+	}
+}
diff --git a/integration/image/commit_test.go b/integration/image/commit_test.go
new file mode 100644
index 0000000..13edbe1
--- /dev/null
+++ b/integration/image/commit_test.go
@@ -0,0 +1,47 @@
+package image
+
+import (
+	"context"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/integration/util/request"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCommitInheritsEnv(t *testing.T) {
+	defer setupTest(t)()
+	client := request.NewAPIClient(t)
+	ctx := context.Background()
+
+	createResp1, err := client.ContainerCreate(ctx, &container.Config{Image: "busybox"}, nil, nil, "")
+	require.NoError(t, err)
+
+	commitResp1, err := client.ContainerCommit(ctx, createResp1.ID, types.ContainerCommitOptions{
+		Changes:   []string{"ENV PATH=/bin"},
+		Reference: "test-commit-image",
+	})
+	require.NoError(t, err)
+
+	image1, _, err := client.ImageInspectWithRaw(ctx, commitResp1.ID)
+	require.NoError(t, err)
+
+	expectedEnv1 := []string{"PATH=/bin"}
+	assert.Equal(t, expectedEnv1, image1.Config.Env)
+
+	createResp2, err := client.ContainerCreate(ctx, &container.Config{Image: image1.ID}, nil, nil, "")
+	require.NoError(t, err)
+
+	commitResp2, err := client.ContainerCommit(ctx, createResp2.ID, types.ContainerCommitOptions{
+		Changes:   []string{"ENV PATH=/usr/bin:$PATH"},
+		Reference: "test-commit-image",
+	})
+	require.NoError(t, err)
+
+	image2, _, err := client.ImageInspectWithRaw(ctx, commitResp2.ID)
+	require.NoError(t, err)
+	expectedEnv2 := []string{"PATH=/usr/bin:/bin"}
+	assert.Equal(t, expectedEnv2, image2.Config.Env)
+}
diff --git a/vendor.conf b/vendor.conf
index 78330a2..2163706 100644
--- a/vendor.conf
+++ b/vendor.conf
@@ -114,7 +114,7 @@
 github.com/stevvooe/ttrpc 76e68349ad9ab4d03d764c713826d31216715e4f
 
 # cluster
-github.com/docker/swarmkit a6519e28ff2a558f5d32b2dab9fcb0882879b398 
+github.com/docker/swarmkit 713d79dc8799b33465c58ed120b870c52eb5eb4f
 github.com/gogo/protobuf v0.4
 github.com/cloudflare/cfssl 7fb22c8cba7ecaf98e4082d22d65800cf45e042a
 github.com/google/certificate-transparency d90e65c3a07988180c5b1ece71791c0b6506826e
diff --git a/vendor/github.com/docker/swarmkit/manager/orchestrator/taskreaper/task_reaper.go b/vendor/github.com/docker/swarmkit/manager/orchestrator/taskreaper/task_reaper.go
index bcef801..d702783 100644
--- a/vendor/github.com/docker/swarmkit/manager/orchestrator/taskreaper/task_reaper.go
+++ b/vendor/github.com/docker/swarmkit/manager/orchestrator/taskreaper/task_reaper.go
@@ -96,10 +96,10 @@
 			// Serviceless tasks can be cleaned up right away since they are not attached to a service.
 			tr.cleanup = append(tr.cleanup, t.ID)
 		}
-		// tasks with desired state REMOVE that have progressed beyond SHUTDOWN can be cleaned up
+		// tasks with desired state REMOVE that have progressed beyond COMPLETE can be cleaned up
 		// right away
 		for _, t := range removeTasks {
-			if t.Status.State >= api.TaskStateShutdown {
+			if t.Status.State >= api.TaskStateCompleted {
 				tr.cleanup = append(tr.cleanup, t.ID)
 			}
 		}
@@ -138,10 +138,10 @@
 				if t.Status.State >= api.TaskStateOrphaned && t.ServiceID == "" {
 					tr.cleanup = append(tr.cleanup, t.ID)
 				}
-				// add tasks that have progressed beyond SHUTDOWN and have desired state REMOVE. These
+				// add tasks that have progressed beyond COMPLETE and have desired state REMOVE. These
 				// tasks are associated with slots that were removed as part of a service scale down
 				// or service removal.
-				if t.DesiredState == api.TaskStateRemove && t.Status.State >= api.TaskStateShutdown {
+				if t.DesiredState == api.TaskStateRemove && t.Status.State >= api.TaskStateCompleted {
 					tr.cleanup = append(tr.cleanup, t.ID)
 				}
 			case api.EventUpdateCluster:
@@ -282,6 +282,8 @@
 
 // Stop stops the TaskReaper and waits for the main loop to exit.
 func (tr *TaskReaper) Stop() {
+	// TODO(dperny) calling stop on the task reaper twice will cause a panic
+	// because we try to close a channel that will already have been closed.
 	close(tr.stopChan)
 	<-tr.doneChan
 }