Browse Source

Add integration test coverage for configs

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Aaron Lehmann 8 years ago
parent
commit
d2c5b6ee9f

+ 56 - 0
integration-cli/daemon/daemon_swarm.go

@@ -118,6 +118,9 @@ type NodeConstructor func(*swarm.Node)
 // SecretConstructor defines a swarm secret constructor
 type SecretConstructor func(*swarm.Secret)
 
+// ConfigConstructor defines a swarm config constructor
+type ConfigConstructor func(*swarm.Config)
+
 // SpecConstructor defines a swarm spec constructor
 type SpecConstructor func(*swarm.Spec)
 
@@ -409,6 +412,59 @@ func (d *Swarm) UpdateSecret(c *check.C, id string, f ...SecretConstructor) {
 	c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
 }
 
+// CreateConfig creates a config given the specified spec
+func (d *Swarm) CreateConfig(c *check.C, configSpec swarm.ConfigSpec) string {
+	status, out, err := d.SockRequest("POST", "/configs/create", configSpec)
+
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf("output: %q", string(out)))
+
+	var scr types.ConfigCreateResponse
+	c.Assert(json.Unmarshal(out, &scr), checker.IsNil)
+	return scr.ID
+}
+
+// ListConfigs returns the list of the current swarm configs
+func (d *Swarm) ListConfigs(c *check.C) []swarm.Config {
+	status, out, err := d.SockRequest("GET", "/configs", nil)
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
+
+	configs := []swarm.Config{}
+	c.Assert(json.Unmarshal(out, &configs), checker.IsNil)
+	return configs
+}
+
+// GetConfig returns a swarm config identified by the specified id
+func (d *Swarm) GetConfig(c *check.C, id string) *swarm.Config {
+	var config swarm.Config
+	status, out, err := d.SockRequest("GET", "/configs/"+id, nil)
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
+	c.Assert(json.Unmarshal(out, &config), checker.IsNil)
+	return &config
+}
+
+// DeleteConfig removes the swarm config identified by the specified id
+func (d *Swarm) DeleteConfig(c *check.C, id string) {
+	status, out, err := d.SockRequest("DELETE", "/configs/"+id, nil)
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusNoContent, check.Commentf("output: %q", string(out)))
+}
+
+// UpdateConfig updates the swarm config identified by the specified id
+// Currently, only label update is supported.
+func (d *Swarm) UpdateConfig(c *check.C, id string, f ...ConfigConstructor) {
+	config := d.GetConfig(c, id)
+	for _, fn := range f {
+		fn(config)
+	}
+	url := fmt.Sprintf("/configs/%s/update?version=%d", config.ID, config.Version.Index)
+	status, out, err := d.SockRequest("POST", url, config.Spec)
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
+}
+
 // GetSwarm returns the current swarm object
 func (d *Swarm) GetSwarm(c *check.C) swarm.Swarm {
 	var sw swarm.Swarm

+ 118 - 0
integration-cli/docker_api_swarm_config_test.go

@@ -0,0 +1,118 @@
+// +build !windows
+
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/integration-cli/checker"
+	"github.com/go-check/check"
+)
+
+func (s *DockerSwarmSuite) TestAPISwarmConfigsEmptyList(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	configs := d.ListConfigs(c)
+	c.Assert(configs, checker.NotNil)
+	c.Assert(len(configs), checker.Equals, 0, check.Commentf("configs: %#v", configs))
+}
+
+func (s *DockerSwarmSuite) TestAPISwarmConfigsCreate(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	configs := d.ListConfigs(c)
+	c.Assert(len(configs), checker.Equals, 1, check.Commentf("configs: %#v", configs))
+	name := configs[0].Spec.Annotations.Name
+	c.Assert(name, checker.Equals, testName, check.Commentf("configs: %s", name))
+}
+
+func (s *DockerSwarmSuite) TestAPISwarmConfigsDelete(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{Annotations: swarm.Annotations{
+		Name: testName,
+	},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	config := d.GetConfig(c, id)
+	c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
+
+	d.DeleteConfig(c, config.ID)
+	status, out, err := d.SockRequest("GET", "/configs/"+id, nil)
+	c.Assert(err, checker.IsNil)
+	c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf("config delete: %s", string(out)))
+}
+
+func (s *DockerSwarmSuite) TestAPISwarmConfigsUpdate(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+			Labels: map[string]string{
+				"test": "test1",
+			},
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	config := d.GetConfig(c, id)
+	c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
+
+	// test UpdateConfig with full ID
+	d.UpdateConfig(c, id, func(s *swarm.Config) {
+		s.Spec.Labels = map[string]string{
+			"test": "test1",
+		}
+	})
+
+	config = d.GetConfig(c, id)
+	c.Assert(config.Spec.Labels["test"], checker.Equals, "test1", check.Commentf("config: %v", config))
+
+	// test UpdateConfig with full name
+	d.UpdateConfig(c, config.Spec.Name, func(s *swarm.Config) {
+		s.Spec.Labels = map[string]string{
+			"test": "test2",
+		}
+	})
+
+	config = d.GetConfig(c, id)
+	c.Assert(config.Spec.Labels["test"], checker.Equals, "test2", check.Commentf("config: %v", config))
+
+	// test UpdateConfig with prefix ID
+	d.UpdateConfig(c, id[:1], func(s *swarm.Config) {
+		s.Spec.Labels = map[string]string{
+			"test": "test3",
+		}
+	})
+
+	config = d.GetConfig(c, id)
+	c.Assert(config.Spec.Labels["test"], checker.Equals, "test3", check.Commentf("config: %v", config))
+
+	// test UpdateConfig in updating Data which is not supported in daemon
+	// this test will produce an error in func UpdateConfig
+	config = d.GetConfig(c, id)
+	config.Spec.Data = []byte("TESTINGDATA2")
+
+	url := fmt.Sprintf("/configs/%s/update?version=%d", config.ID, config.Version.Index)
+	status, out, err := d.SockRequest("POST", url, config.Spec)
+
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
+	c.Assert(status, checker.Equals, http.StatusInternalServerError, check.Commentf("output: %q", string(out)))
+}

+ 131 - 0
integration-cli/docker_cli_config_create_test.go

@@ -0,0 +1,131 @@
+// +build !windows
+
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/integration-cli/checker"
+	"github.com/go-check/check"
+)
+
+func (s *DockerSwarmSuite) TestConfigCreate(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	config := d.GetConfig(c, id)
+	c.Assert(config.Spec.Name, checker.Equals, testName)
+}
+
+func (s *DockerSwarmSuite) TestConfigCreateWithLabels(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+			Labels: map[string]string{
+				"key1": "value1",
+				"key2": "value2",
+			},
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	config := d.GetConfig(c, id)
+	c.Assert(config.Spec.Name, checker.Equals, testName)
+	c.Assert(len(config.Spec.Labels), checker.Equals, 2)
+	c.Assert(config.Spec.Labels["key1"], checker.Equals, "value1")
+	c.Assert(config.Spec.Labels["key2"], checker.Equals, "value2")
+}
+
+// Test case for 28884
+func (s *DockerSwarmSuite) TestConfigCreateResolve(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	name := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: name,
+		},
+		Data: []byte("foo"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	fake := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: id,
+		},
+		Data: []byte("fake foo"),
+	})
+	c.Assert(fake, checker.Not(checker.Equals), "", check.Commentf("configs: %s", fake))
+
+	out, err := d.Cmd("config", "ls")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Contains, name)
+	c.Assert(out, checker.Contains, fake)
+
+	out, err = d.Cmd("config", "rm", id)
+	c.Assert(out, checker.Contains, id)
+
+	// Fake one will remain
+	out, err = d.Cmd("config", "ls")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Not(checker.Contains), name)
+	c.Assert(out, checker.Contains, fake)
+
+	// Remove based on name prefix of the fake one
+	// (which is the same as the ID of foo one) should not work
+	// as search is only done based on:
+	// - Full ID
+	// - Full Name
+	// - Partial ID (prefix)
+	out, err = d.Cmd("config", "rm", id[:5])
+	c.Assert(out, checker.Not(checker.Contains), id)
+	out, err = d.Cmd("config", "ls")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Not(checker.Contains), name)
+	c.Assert(out, checker.Contains, fake)
+
+	// Remove based on ID prefix of the fake one should succeed
+	out, err = d.Cmd("config", "rm", fake[:5])
+	c.Assert(out, checker.Contains, fake[:5])
+	out, err = d.Cmd("config", "ls")
+	c.Assert(err, checker.IsNil)
+	c.Assert(out, checker.Not(checker.Contains), name)
+	c.Assert(out, checker.Not(checker.Contains), id)
+	c.Assert(out, checker.Not(checker.Contains), fake)
+}
+
+func (s *DockerSwarmSuite) TestConfigCreateWithFile(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testFile, err := ioutil.TempFile("", "configCreateTest")
+	c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary file"))
+	defer os.Remove(testFile.Name())
+
+	testData := "TESTINGDATA"
+	_, err = testFile.Write([]byte(testData))
+	c.Assert(err, checker.IsNil, check.Commentf("failed to write to temporary file"))
+
+	testName := "test_config"
+	out, err := d.Cmd("config", "create", testName, testFile.Name())
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "", check.Commentf(out))
+
+	id := strings.TrimSpace(out)
+	config := d.GetConfig(c, id)
+	c.Assert(config.Spec.Name, checker.Equals, testName)
+}

+ 68 - 0
integration-cli/docker_cli_config_inspect_test.go

@@ -0,0 +1,68 @@
+// +build !windows
+
+package main
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/integration-cli/checker"
+	"github.com/go-check/check"
+)
+
+func (s *DockerSwarmSuite) TestConfigInspect(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	config := d.GetConfig(c, id)
+	c.Assert(config.Spec.Name, checker.Equals, testName)
+
+	out, err := d.Cmd("config", "inspect", testName)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	var configs []swarm.Config
+	c.Assert(json.Unmarshal([]byte(out), &configs), checker.IsNil)
+	c.Assert(configs, checker.HasLen, 1)
+}
+
+func (s *DockerSwarmSuite) TestConfigInspectMultiple(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testNames := []string{
+		"test0",
+		"test1",
+	}
+	for _, n := range testNames {
+		id := d.CreateConfig(c, swarm.ConfigSpec{
+			Annotations: swarm.Annotations{
+				Name: n,
+			},
+			Data: []byte("TESTINGDATA"),
+		})
+		c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+		config := d.GetConfig(c, id)
+		c.Assert(config.Spec.Name, checker.Equals, n)
+
+	}
+
+	args := []string{
+		"config",
+		"inspect",
+	}
+	args = append(args, testNames...)
+	out, err := d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	var configs []swarm.Config
+	c.Assert(json.Unmarshal([]byte(out), &configs), checker.IsNil)
+	c.Assert(configs, checker.HasLen, 2)
+}

+ 125 - 0
integration-cli/docker_cli_config_ls_test.go

@@ -0,0 +1,125 @@
+// +build !windows
+
+package main
+
+import (
+	"strings"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/integration-cli/checker"
+	"github.com/go-check/check"
+)
+
+func (s *DockerSwarmSuite) TestConfigList(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testName0 := "test0"
+	testName1 := "test1"
+
+	// create config test0
+	id0 := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name:   testName0,
+			Labels: map[string]string{"type": "test"},
+		},
+		Data: []byte("TESTINGDATA0"),
+	})
+	c.Assert(id0, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id0))
+
+	config := d.GetConfig(c, id0)
+	c.Assert(config.Spec.Name, checker.Equals, testName0)
+
+	// create config test1
+	id1 := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name:   testName1,
+			Labels: map[string]string{"type": "production"},
+		},
+		Data: []byte("TESTINGDATA1"),
+	})
+	c.Assert(id1, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id1))
+
+	config = d.GetConfig(c, id1)
+	c.Assert(config.Spec.Name, checker.Equals, testName1)
+
+	// test by command `docker config ls`
+	out, err := d.Cmd("config", "ls")
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
+
+	// test filter by name `docker config ls --filter name=xxx`
+	args := []string{
+		"config",
+		"ls",
+		"--filter",
+		"name=test0",
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
+
+	// test filter by id `docker config ls --filter id=xxx`
+	args = []string{
+		"config",
+		"ls",
+		"--filter",
+		"id=" + id1,
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
+
+	// test filter by label `docker config ls --filter label=xxx`
+	args = []string{
+		"config",
+		"ls",
+		"--filter",
+		"label=type",
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
+
+	args = []string{
+		"config",
+		"ls",
+		"--filter",
+		"label=type=test",
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
+
+	args = []string{
+		"config",
+		"ls",
+		"--filter",
+		"label=type=production",
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
+	c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
+
+	// test invalid filter `docker config ls --filter noexisttype=xxx`
+	args = []string{
+		"config",
+		"ls",
+		"--filter",
+		"noexisttype=test0",
+	}
+	out, err = d.Cmd(args...)
+	c.Assert(err, checker.NotNil, check.Commentf(out))
+
+	c.Assert(strings.TrimSpace(out), checker.Contains, "Error response from daemon: Invalid filter 'noexisttype'")
+}

+ 147 - 0
integration-cli/docker_cli_service_create_test.go

@@ -211,6 +211,153 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretReferencedTwice(c *check.C
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 }
 
+func (s *DockerSwarmSuite) TestServiceCreateWithConfigSimple(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	serviceName := "test-service-config"
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	out, err := d.Cmd("service", "create", "--name", serviceName, "--config", testName, "busybox", "top")
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
+	c.Assert(err, checker.IsNil)
+
+	var refs []swarm.ConfigReference
+	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+	c.Assert(refs, checker.HasLen, 1)
+
+	c.Assert(refs[0].ConfigName, checker.Equals, testName)
+	c.Assert(refs[0].File, checker.Not(checker.IsNil))
+	c.Assert(refs[0].File.Name, checker.Equals, testName)
+	c.Assert(refs[0].File.UID, checker.Equals, "0")
+	c.Assert(refs[0].File.GID, checker.Equals, "0")
+
+	out, err = d.Cmd("service", "rm", serviceName)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+	d.DeleteConfig(c, testName)
+}
+
+func (s *DockerSwarmSuite) TestServiceCreateWithConfigSourceTargetPaths(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	testPaths := map[string]string{
+		"app":             "/etc/config",
+		"test_config":     "test_config",
+		"relative_config": "relative/config",
+	}
+
+	var configFlags []string
+
+	for testName, testTarget := range testPaths {
+		id := d.CreateConfig(c, swarm.ConfigSpec{
+			Annotations: swarm.Annotations{
+				Name: testName,
+			},
+			Data: []byte("TESTINGDATA " + testName + " " + testTarget),
+		})
+		c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+		configFlags = append(configFlags, "--config", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
+	}
+
+	serviceName := "svc"
+	serviceCmd := []string{"service", "create", "--name", serviceName}
+	serviceCmd = append(serviceCmd, configFlags...)
+	serviceCmd = append(serviceCmd, "busybox", "top")
+	out, err := d.Cmd(serviceCmd...)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
+	c.Assert(err, checker.IsNil)
+
+	var refs []swarm.ConfigReference
+	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+	c.Assert(refs, checker.HasLen, len(testPaths))
+
+	var tasks []swarm.Task
+	waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
+		tasks = d.GetServiceTasks(c, serviceName)
+		return len(tasks) > 0, nil
+	}, checker.Equals, true)
+
+	task := tasks[0]
+	waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
+		if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" {
+			task = d.GetTask(c, task.ID)
+		}
+		return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
+	}, checker.Equals, true)
+
+	for testName, testTarget := range testPaths {
+		path := testTarget
+		if !filepath.IsAbs(path) {
+			path = filepath.Join("/", path)
+		}
+		out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path)
+		c.Assert(err, checker.IsNil)
+		c.Assert(out, checker.Equals, "TESTINGDATA "+testName+" "+testTarget)
+	}
+
+	out, err = d.Cmd("service", "rm", serviceName)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+}
+
+func (s *DockerSwarmSuite) TestServiceCreateWithConfigReferencedTwice(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: "myconfig",
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+
+	serviceName := "svc"
+	out, err := d.Cmd("service", "create", "--name", serviceName, "--config", "source=myconfig,target=target1", "--config", "source=myconfig,target=target2", "busybox", "top")
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
+	c.Assert(err, checker.IsNil)
+
+	var refs []swarm.ConfigReference
+	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+	c.Assert(refs, checker.HasLen, 2)
+
+	var tasks []swarm.Task
+	waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
+		tasks = d.GetServiceTasks(c, serviceName)
+		return len(tasks) > 0, nil
+	}, checker.Equals, true)
+
+	task := tasks[0]
+	waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
+		if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" {
+			task = d.GetTask(c, task.ID)
+		}
+		return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
+	}, checker.Equals, true)
+
+	for _, target := range []string{"target1", "target2"} {
+		c.Assert(err, checker.IsNil, check.Commentf(out))
+		path := filepath.Join("/", target)
+		out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path)
+		c.Assert(err, checker.IsNil)
+		c.Assert(out, checker.Equals, "TESTINGDATA")
+	}
+
+	out, err = d.Cmd("service", "rm", serviceName)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+}
+
 func (s *DockerSwarmSuite) TestServiceCreateMountTmpfs(c *check.C) {
 	d := s.AddDaemon(c, true, true)
 	out, err := d.Cmd("service", "create", "--detach=true", "--mount", "type=tmpfs,target=/foo,tmpfs-size=1MB", "busybox", "sh", "-c", "mount | grep foo; tail -f /dev/null")

+ 42 - 0
integration-cli/docker_cli_service_update_test.go

@@ -128,3 +128,45 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
 	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
 	c.Assert(refs, checker.HasLen, 0)
 }
+
+func (s *DockerSwarmSuite) TestServiceUpdateConfigs(c *check.C) {
+	d := s.AddDaemon(c, true, true)
+	testName := "test_config"
+	id := d.CreateConfig(c, swarm.ConfigSpec{
+		Annotations: swarm.Annotations{
+			Name: testName,
+		},
+		Data: []byte("TESTINGDATA"),
+	})
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
+	testTarget := "/testing"
+	serviceName := "test"
+
+	out, err := d.Cmd("service", "create", "--name", serviceName, "busybox", "top")
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	// add config
+	out, err = d.CmdRetryOutOfSequence("service", "update", "test", "--config-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
+	c.Assert(err, checker.IsNil)
+
+	var refs []swarm.ConfigReference
+	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+	c.Assert(refs, checker.HasLen, 1)
+
+	c.Assert(refs[0].ConfigName, checker.Equals, testName)
+	c.Assert(refs[0].File, checker.Not(checker.IsNil))
+	c.Assert(refs[0].File.Name, checker.Equals, testTarget)
+
+	// remove
+	out, err = d.CmdRetryOutOfSequence("service", "update", "test", "--config-rm", testName)
+	c.Assert(err, checker.IsNil, check.Commentf(out))
+
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
+	c.Assert(err, checker.IsNil)
+
+	c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+	c.Assert(refs, checker.HasLen, 0)
+}