Browse Source

Merge pull request #32336 from aaronlehmann/configs

Configuration files for services
Victor Vieux 8 years ago
parent
commit
69c35dad8e
48 changed files with 1928 additions and 51 deletions
  1. 11 0
      api/server/router/swarm/backend.go
  2. 10 0
      api/server/router/swarm/cluster.go
  3. 71 0
      api/server/router/swarm/cluster_routes.go
  4. 31 0
      api/types/swarm/config.go
  5. 1 0
      api/types/swarm/container.go
  6. 12 0
      api/types/types.go
  7. 22 0
      client/config_create.go
  8. 57 0
      client/config_create_test.go
  9. 34 0
      client/config_inspect.go
  10. 65 0
      client/config_inspect_test.go
  11. 35 0
      client/config_list.go
  12. 94 0
      client/config_list_test.go
  13. 10 0
      client/config_remove.go
  14. 47 0
      client/config_remove_test.go
  15. 18 0
      client/config_update.go
  16. 49 0
      client/config_update_test.go
  17. 22 0
      client/errors.go
  18. 10 0
      client/interface.go
  19. 13 1
      container/container.go
  20. 17 0
      container/container_unix.go
  21. 117 0
      daemon/cluster/configs.go
  22. 61 0
      daemon/cluster/convert/config.go
  23. 51 0
      daemon/cluster/convert/container.go
  24. 2 1
      daemon/cluster/executor/backend.go
  25. 15 10
      daemon/cluster/executor/container/adapter.go
  26. 2 2
      daemon/cluster/executor/container/attachment.go
  27. 2 2
      daemon/cluster/executor/container/controller.go
  28. 14 8
      daemon/cluster/executor/container/executor.go
  29. 16 0
      daemon/cluster/filters.go
  30. 45 0
      daemon/cluster/filters_test.go
  31. 36 0
      daemon/cluster/helpers.go
  32. 23 0
      daemon/configs.go
  33. 7 0
      daemon/configs_linux.go
  34. 7 0
      daemon/configs_unsupported.go
  35. 82 12
      daemon/container_operations_unix.go
  36. 17 0
      daemon/dependency.go
  37. 6 0
      daemon/oci_linux.go
  38. 0 13
      daemon/secrets.go
  39. 9 0
      docs/reference/commandline/cli.md
  40. 2 2
      hack/dockerfile/binaries-commits
  41. 56 0
      integration-cli/daemon/daemon_swarm.go
  42. 118 0
      integration-cli/docker_api_swarm_config_test.go
  43. 131 0
      integration-cli/docker_cli_config_create_test.go
  44. 68 0
      integration-cli/docker_cli_config_inspect_test.go
  45. 125 0
      integration-cli/docker_cli_config_ls_test.go
  46. 147 0
      integration-cli/docker_cli_service_create_test.go
  47. 42 0
      integration-cli/docker_cli_service_update_test.go
  48. 98 0
      opts/config.go

+ 11 - 0
api/server/router/swarm/backend.go

@@ -16,21 +16,32 @@ type Backend interface {
 	Update(uint64, types.Spec, types.UpdateFlags) error
 	GetUnlockKey() (string, error)
 	UnlockSwarm(req types.UnlockRequest) error
+
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
 	GetService(idOrName string, insertDefaults bool) (types.Service, error)
 	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
 	UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
 	RemoveService(string) error
+
 	ServiceLogs(context.Context, *backend.LogSelector, *basictypes.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
+
 	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
 	GetNode(string) (types.Node, error)
 	UpdateNode(string, uint64, types.NodeSpec) error
 	RemoveNode(string, bool) error
+
 	GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
 	GetTask(string) (types.Task, error)
+
 	GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
 	CreateSecret(s types.SecretSpec) (string, error)
 	RemoveSecret(idOrName string) error
 	GetSecret(id string) (types.Secret, error)
 	UpdateSecret(idOrName string, version uint64, spec types.SecretSpec) error
+
+	GetConfigs(opts basictypes.ConfigListOptions) ([]types.Config, error)
+	CreateConfig(s types.ConfigSpec) (string, error)
+	RemoveConfig(id string) error
+	GetConfig(id string) (types.Config, error)
+	UpdateConfig(idOrName string, version uint64, spec types.ConfigSpec) error
 }

+ 10 - 0
api/server/router/swarm/cluster.go

@@ -31,23 +31,33 @@ func (sr *swarmRouter) initRoutes() {
 		router.NewGetRoute("/swarm/unlockkey", sr.getUnlockKey),
 		router.NewPostRoute("/swarm/update", sr.updateCluster),
 		router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
+
 		router.NewGetRoute("/services", sr.getServices),
 		router.NewGetRoute("/services/{id}", sr.getService),
 		router.NewPostRoute("/services/create", sr.createService),
 		router.NewPostRoute("/services/{id}/update", sr.updateService),
 		router.NewDeleteRoute("/services/{id}", sr.removeService),
 		router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs, router.WithCancel),
+
 		router.NewGetRoute("/nodes", sr.getNodes),
 		router.NewGetRoute("/nodes/{id}", sr.getNode),
 		router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
 		router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
+
 		router.NewGetRoute("/tasks", sr.getTasks),
 		router.NewGetRoute("/tasks/{id}", sr.getTask),
 		router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs, router.WithCancel),
+
 		router.NewGetRoute("/secrets", sr.getSecrets),
 		router.NewPostRoute("/secrets/create", sr.createSecret),
 		router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
 		router.NewGetRoute("/secrets/{id}", sr.getSecret),
 		router.NewPostRoute("/secrets/{id}/update", sr.updateSecret),
+
+		router.NewGetRoute("/configs", sr.getConfigs),
+		router.NewPostRoute("/configs/create", sr.createConfig),
+		router.NewDeleteRoute("/configs/{id}", sr.removeConfig),
+		router.NewGetRoute("/configs/{id}", sr.getConfig),
+		router.NewPostRoute("/configs/{id}/update", sr.updateConfig),
 	}
 }

+ 71 - 0
api/server/router/swarm/cluster_routes.go

@@ -408,3 +408,74 @@ func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter,
 
 	return nil
 }
+
+func (sr *swarmRouter) getConfigs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+	filters, err := filters.FromParam(r.Form.Get("filters"))
+	if err != nil {
+		return err
+	}
+
+	configs, err := sr.backend.GetConfigs(basictypes.ConfigListOptions{Filters: filters})
+	if err != nil {
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, configs)
+}
+
+func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var config types.ConfigSpec
+	if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
+		return err
+	}
+
+	id, err := sr.backend.CreateConfig(config)
+	if err != nil {
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.ConfigCreateResponse{
+		ID: id,
+	})
+}
+
+func (sr *swarmRouter) removeConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := sr.backend.RemoveConfig(vars["id"]); err != nil {
+		return err
+	}
+	w.WriteHeader(http.StatusNoContent)
+
+	return nil
+}
+
+func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	config, err := sr.backend.GetConfig(vars["id"])
+	if err != nil {
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, config)
+}
+
+func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var config types.ConfigSpec
+	if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
+		return errors.NewBadRequestError(err)
+	}
+
+	rawVersion := r.URL.Query().Get("version")
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
+	if err != nil {
+		return errors.NewBadRequestError(fmt.Errorf("invalid config version"))
+	}
+
+	id := vars["id"]
+	if err := sr.backend.UpdateConfig(id, version, config); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 31 - 0
api/types/swarm/config.go

@@ -0,0 +1,31 @@
+package swarm
+
+import "os"
+
+// Config represents a config.
+type Config struct {
+	ID string
+	Meta
+	Spec ConfigSpec
+}
+
+// ConfigSpec represents a config specification from a config in swarm
+type ConfigSpec struct {
+	Annotations
+	Data []byte `json:",omitempty"`
+}
+
+// ConfigReferenceFileTarget is a file target in a config reference
+type ConfigReferenceFileTarget struct {
+	Name string
+	UID  string
+	GID  string
+	Mode os.FileMode
+}
+
+// ConfigReference is a reference to a config in swarm
+type ConfigReference struct {
+	File       *ConfigReferenceFileTarget
+	ConfigID   string
+	ConfigName string
+}

+ 1 - 0
api/types/swarm/container.go

@@ -68,4 +68,5 @@ type ContainerSpec struct {
 	Hosts     []string           `json:",omitempty"`
 	DNSConfig *DNSConfig         `json:",omitempty"`
 	Secrets   []*SecretReference `json:",omitempty"`
+	Configs   []*ConfigReference `json:",omitempty"`
 }

+ 12 - 0
api/types/types.go

@@ -522,6 +522,18 @@ type SecretListOptions struct {
 	Filters filters.Args
 }
 
+// ConfigCreateResponse contains the information returned to a client
+// on the creation of a new config.
+type ConfigCreateResponse struct {
+	// ID is the id of the created config.
+	ID string
+}
+
+// ConfigListOptions holds parameters to list configs
+type ConfigListOptions struct {
+	Filters filters.Args
+}
+
 // PushResult contains the tag, manifest digest, and manifest size from the
 // push. It's used to signal this information to the trust code in the client
 // so it can sign the manifest if necessary.

+ 22 - 0
client/config_create.go

@@ -0,0 +1,22 @@
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// ConfigCreate creates a new Config.
+func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
+	var response types.ConfigCreateResponse
+	resp, err := cli.post(ctx, "/configs/create", nil, config, nil)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}

+ 57 - 0
client/config_create_test.go

@@ -0,0 +1,57 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestConfigCreateError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestConfigCreate(t *testing.T) {
+	expectedURL := "/configs/create"
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "POST" {
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+			}
+			b, err := json.Marshal(types.ConfigCreateResponse{
+				ID: "test_config",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusCreated,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "test_config" {
+		t.Fatalf("expected `test_config`, got %s", r.ID)
+	}
+}

+ 34 - 0
client/config_inspect.go

@@ -0,0 +1,34 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// ConfigInspectWithRaw returns the config information with raw data
+func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
+	resp, err := cli.get(ctx, "/configs/"+id, nil, nil)
+	if err != nil {
+		if resp.statusCode == http.StatusNotFound {
+			return swarm.Config{}, nil, configNotFoundError{id}
+		}
+		return swarm.Config{}, nil, err
+	}
+	defer ensureReaderClosed(resp)
+
+	body, err := ioutil.ReadAll(resp.body)
+	if err != nil {
+		return swarm.Config{}, nil, err
+	}
+
+	var config swarm.Config
+	rdr := bytes.NewReader(body)
+	err = json.NewDecoder(rdr).Decode(&config)
+
+	return config, body, err
+}

+ 65 - 0
client/config_inspect_test.go

@@ -0,0 +1,65 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestConfigInspectError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, _, err := client.ConfigInspectWithRaw(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestConfigInspectConfigNotFound(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
+	}
+
+	_, _, err := client.ConfigInspectWithRaw(context.Background(), "unknown")
+	if err == nil || !IsErrConfigNotFound(err) {
+		t.Fatalf("expected a configNotFoundError error, got %v", err)
+	}
+}
+
+func TestConfigInspect(t *testing.T) {
+	expectedURL := "/configs/config_id"
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			content, err := json.Marshal(swarm.Config{
+				ID: "config_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
+			}, nil
+		}),
+	}
+
+	configInspect, _, err := client.ConfigInspectWithRaw(context.Background(), "config_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if configInspect.ID != "config_id" {
+		t.Fatalf("expected `config_id`, got %s", configInspect.ID)
+	}
+}

+ 35 - 0
client/config_list.go

@@ -0,0 +1,35 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// ConfigList returns the list of configs.
+func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
+	query := url.Values{}
+
+	if options.Filters.Len() > 0 {
+		filterJSON, err := filters.ToParam(options.Filters)
+		if err != nil {
+			return nil, err
+		}
+
+		query.Set("filters", filterJSON)
+	}
+
+	resp, err := cli.get(ctx, "/configs", query, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var configs []swarm.Config
+	err = json.NewDecoder(resp.body).Decode(&configs)
+	ensureReaderClosed(resp)
+	return configs, err
+}

+ 94 - 0
client/config_list_test.go

@@ -0,0 +1,94 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestConfigListError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, err := client.ConfigList(context.Background(), types.ConfigListOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestConfigList(t *testing.T) {
+	expectedURL := "/configs"
+
+	filters := filters.NewArgs()
+	filters.Add("label", "label1")
+	filters.Add("label", "label2")
+
+	listCases := []struct {
+		options             types.ConfigListOptions
+		expectedQueryParams map[string]string
+	}{
+		{
+			options: types.ConfigListOptions{},
+			expectedQueryParams: map[string]string{
+				"filters": "",
+			},
+		},
+		{
+			options: types.ConfigListOptions{
+				Filters: filters,
+			},
+			expectedQueryParams: map[string]string{
+				"filters": `{"label":{"label1":true,"label2":true}}`,
+			},
+		},
+	}
+	for _, listCase := range listCases {
+		client := &Client{
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+				}
+				query := req.URL.Query()
+				for key, expected := range listCase.expectedQueryParams {
+					actual := query.Get(key)
+					if actual != expected {
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
+					}
+				}
+				content, err := json.Marshal([]swarm.Config{
+					{
+						ID: "config_id1",
+					},
+					{
+						ID: "config_id2",
+					},
+				})
+				if err != nil {
+					return nil, err
+				}
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
+				}, nil
+			}),
+		}
+
+		configs, err := client.ConfigList(context.Background(), listCase.options)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(configs) != 2 {
+			t.Fatalf("expected 2 configs, got %v", configs)
+		}
+	}
+}

+ 10 - 0
client/config_remove.go

@@ -0,0 +1,10 @@
+package client
+
+import "golang.org/x/net/context"
+
+// ConfigRemove removes a Config.
+func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
+	resp, err := cli.delete(ctx, "/configs/"+id, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 47 - 0
client/config_remove_test.go

@@ -0,0 +1,47 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestConfigRemoveError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	err := client.ConfigRemove(context.Background(), "config_id")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestConfigRemove(t *testing.T) {
+	expectedURL := "/configs/config_id"
+
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "DELETE" {
+				return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+			}, nil
+		}),
+	}
+
+	err := client.ConfigRemove(context.Background(), "config_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 18 - 0
client/config_update.go

@@ -0,0 +1,18 @@
+package client
+
+import (
+	"net/url"
+	"strconv"
+
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// ConfigUpdate attempts to updates a Config
+func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
+	query := url.Values{}
+	query.Set("version", strconv.FormatUint(version.Index, 10))
+	resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 49 - 0
client/config_update_test.go

@@ -0,0 +1,49 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types/swarm"
+)
+
+func TestConfigUpdateError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestConfigUpdate(t *testing.T) {
+	expectedURL := "/configs/config_id/update"
+
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "POST" {
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+			}, nil
+		}),
+	}
+
+	err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 22 - 0
client/errors.go

@@ -256,6 +256,28 @@ func IsErrSecretNotFound(err error) bool {
 	return ok
 }
 
+// configNotFoundError implements an error returned when a config is not found.
+type configNotFoundError struct {
+	name string
+}
+
+// Error returns a string representation of a configNotFoundError
+func (e configNotFoundError) Error() string {
+	return fmt.Sprintf("Error: no such config: %s", e.name)
+}
+
+// NotFound indicates that this error type is of NotFound
+func (e configNotFoundError) NotFound() bool {
+	return true
+}
+
+// IsErrConfigNotFound returns true if the error is caused
+// when a config is not found.
+func IsErrConfigNotFound(err error) bool {
+	_, ok := err.(configNotFoundError)
+	return ok
+}
+
 // pluginNotFoundError implements an error returned when a plugin is not in the docker host.
 type pluginNotFoundError struct {
 	name string

+ 10 - 0
client/interface.go

@@ -18,6 +18,7 @@ import (
 
 // CommonAPIClient is the common methods between stable and experimental versions of APIClient.
 type CommonAPIClient interface {
+	ConfigAPIClient
 	ContainerAPIClient
 	ImageAPIClient
 	NodeAPIClient
@@ -171,3 +172,12 @@ type SecretAPIClient interface {
 	SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
 	SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
 }
+
+// ConfigAPIClient defines API client methods for configs
+type ConfigAPIClient interface {
+	ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error)
+	ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error)
+	ConfigRemove(ctx context.Context, id string) error
+	ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error)
+	ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error
+}

+ 13 - 1
container/container.go

@@ -87,8 +87,9 @@ type CommonContainer struct {
 	MountPoints            map[string]*volume.MountPoint
 	HostConfig             *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
 	ExecCommands           *exec.Store                `json:"-"`
-	SecretStore            agentexec.SecretGetter     `json:"-"`
+	DependencyStore        agentexec.DependencyGetter `json:"-"`
 	SecretReferences       []*swarmtypes.SecretReference
+	ConfigReferences       []*swarmtypes.ConfigReference
 	// logDriver for closing
 	LogDriver      logger.Logger  `json:"-"`
 	LogCopier      *logger.Copier `json:"-"`
@@ -966,3 +967,14 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string {
 
 	return filepath.Join(containerSecretMountPath, r.File.Name)
 }
+
+// ConfigsDirPath returns the path to the directory where configs are stored on
+// disk.
+func (container *Container) ConfigsDirPath() string {
+	return filepath.Join(container.Root, "configs")
+}
+
+// ConfigFilePath returns the path to the on-disk location of a config.
+func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string {
+	return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID)
+}

+ 17 - 0
container/container_unix.go

@@ -277,6 +277,23 @@ func (container *Container) UnmountSecrets() error {
 	return detachMounted(container.SecretMountPath())
 }
 
+// ConfigMounts returns the mounts for configs.
+func (container *Container) ConfigMounts() []Mount {
+	var mounts []Mount
+	for _, configRef := range container.ConfigReferences {
+		if configRef.File == nil {
+			continue
+		}
+		mounts = append(mounts, Mount{
+			Source:      container.ConfigFilePath(*configRef),
+			Destination: configRef.File.Name,
+			Writable:    false,
+		})
+	}
+
+	return mounts
+}
+
 // UpdateContainer updates configuration of a container.
 func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
 	container.Lock()

+ 117 - 0
daemon/cluster/configs.go

@@ -0,0 +1,117 @@
+package cluster
+
+import (
+	apitypes "github.com/docker/docker/api/types"
+	types "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/daemon/cluster/convert"
+	swarmapi "github.com/docker/swarmkit/api"
+	"golang.org/x/net/context"
+)
+
+// GetConfig returns a config from a managed swarm cluster
+func (c *Cluster) GetConfig(input string) (types.Config, error) {
+	var config *swarmapi.Config
+
+	if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
+		s, err := getConfig(ctx, state.controlClient, input)
+		if err != nil {
+			return err
+		}
+		config = s
+		return nil
+	}); err != nil {
+		return types.Config{}, err
+	}
+	return convert.ConfigFromGRPC(config), nil
+}
+
+// GetConfigs returns all configs of a managed swarm cluster.
+func (c *Cluster) GetConfigs(options apitypes.ConfigListOptions) ([]types.Config, error) {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	state := c.currentNodeState()
+	if !state.IsActiveManager() {
+		return nil, c.errNoManager(state)
+	}
+
+	filters, err := newListConfigsFilters(options.Filters)
+	if err != nil {
+		return nil, err
+	}
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	r, err := state.controlClient.ListConfigs(ctx,
+		&swarmapi.ListConfigsRequest{Filters: filters})
+	if err != nil {
+		return nil, err
+	}
+
+	configs := []types.Config{}
+
+	for _, config := range r.Configs {
+		configs = append(configs, convert.ConfigFromGRPC(config))
+	}
+
+	return configs, nil
+}
+
+// CreateConfig creates a new config in a managed swarm cluster.
+func (c *Cluster) CreateConfig(s types.ConfigSpec) (string, error) {
+	var resp *swarmapi.CreateConfigResponse
+	if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
+		configSpec := convert.ConfigSpecToGRPC(s)
+
+		r, err := state.controlClient.CreateConfig(ctx,
+			&swarmapi.CreateConfigRequest{Spec: &configSpec})
+		if err != nil {
+			return err
+		}
+		resp = r
+		return nil
+	}); err != nil {
+		return "", err
+	}
+	return resp.Config.ID, nil
+}
+
+// RemoveConfig removes a config from a managed swarm cluster.
+func (c *Cluster) RemoveConfig(input string) error {
+	return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
+		config, err := getConfig(ctx, state.controlClient, input)
+		if err != nil {
+			return err
+		}
+
+		req := &swarmapi.RemoveConfigRequest{
+			ConfigID: config.ID,
+		}
+
+		_, err = state.controlClient.RemoveConfig(ctx, req)
+		return err
+	})
+}
+
+// UpdateConfig updates a config in a managed swarm cluster.
+// Note: this is not exposed to the CLI but is available from the API only
+func (c *Cluster) UpdateConfig(input string, version uint64, spec types.ConfigSpec) error {
+	return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
+		config, err := getConfig(ctx, state.controlClient, input)
+		if err != nil {
+			return err
+		}
+
+		configSpec := convert.ConfigSpecToGRPC(spec)
+
+		_, err = state.controlClient.UpdateConfig(ctx,
+			&swarmapi.UpdateConfigRequest{
+				ConfigID: config.ID,
+				ConfigVersion: &swarmapi.Version{
+					Index: version,
+				},
+				Spec: &configSpec,
+			})
+		return err
+	})
+}

+ 61 - 0
daemon/cluster/convert/config.go

@@ -0,0 +1,61 @@
+package convert
+
+import (
+	swarmtypes "github.com/docker/docker/api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	gogotypes "github.com/gogo/protobuf/types"
+)
+
+// ConfigFromGRPC converts a grpc Config to a Config.
+func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
+	config := swarmtypes.Config{
+		ID: s.ID,
+		Spec: swarmtypes.ConfigSpec{
+			Annotations: annotationsFromGRPC(s.Spec.Annotations),
+			Data:        s.Spec.Data,
+		},
+	}
+
+	config.Version.Index = s.Meta.Version.Index
+	// Meta
+	config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
+	config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
+
+	return config
+}
+
+// ConfigSpecToGRPC converts Config to a grpc Config.
+func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
+	return swarmapi.ConfigSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   s.Name,
+			Labels: s.Labels,
+		},
+		Data: s.Data,
+	}
+}
+
+// ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference
+func ConfigReferencesFromGRPC(s []*swarmapi.ConfigReference) []*swarmtypes.ConfigReference {
+	refs := []*swarmtypes.ConfigReference{}
+
+	for _, r := range s {
+		ref := &swarmtypes.ConfigReference{
+			ConfigID:   r.ConfigID,
+			ConfigName: r.ConfigName,
+		}
+
+		if t, ok := r.Target.(*swarmapi.ConfigReference_File); ok {
+			ref.File = &swarmtypes.ConfigReferenceFileTarget{
+				Name: t.File.Name,
+				UID:  t.File.UID,
+				GID:  t.File.GID,
+				Mode: t.File.Mode,
+			}
+		}
+
+		refs = append(refs, ref)
+	}
+
+	return refs
+}

+ 51 - 0
daemon/cluster/convert/container.go

@@ -30,6 +30,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
 		ReadOnly:   c.ReadOnly,
 		Hosts:      c.Hosts,
 		Secrets:    secretReferencesFromGRPC(c.Secrets),
+		Configs:    configReferencesFromGRPC(c.Configs),
 	}
 
 	if c.DNSConfig != nil {
@@ -137,6 +138,7 @@ func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretRefer
 
 	return refs
 }
+
 func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
 	refs := make([]*types.SecretReference, 0, len(sr))
 	for _, s := range sr {
@@ -161,6 +163,54 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef
 	return refs
 }
 
+func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigReference {
+	refs := make([]*swarmapi.ConfigReference, 0, len(sr))
+	for _, s := range sr {
+		ref := &swarmapi.ConfigReference{
+			ConfigID:   s.ConfigID,
+			ConfigName: s.ConfigName,
+		}
+		if s.File != nil {
+			ref.Target = &swarmapi.ConfigReference_File{
+				File: &swarmapi.FileTarget{
+					Name: s.File.Name,
+					UID:  s.File.UID,
+					GID:  s.File.GID,
+					Mode: s.File.Mode,
+				},
+			}
+		}
+
+		refs = append(refs, ref)
+	}
+
+	return refs
+}
+
+func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigReference {
+	refs := make([]*types.ConfigReference, 0, len(sr))
+	for _, s := range sr {
+		target := s.GetFile()
+		if target == nil {
+			// not a file target
+			logrus.Warnf("config target not a file: config=%s", s.ConfigID)
+			continue
+		}
+		refs = append(refs, &types.ConfigReference{
+			File: &types.ConfigReferenceFileTarget{
+				Name: target.Name,
+				UID:  target.UID,
+				GID:  target.GID,
+				Mode: target.Mode,
+			},
+			ConfigID:   s.ConfigID,
+			ConfigName: s.ConfigName,
+		})
+	}
+
+	return refs
+}
+
 func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 	containerSpec := &swarmapi.ContainerSpec{
 		Image:      c.Image,
@@ -178,6 +228,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		ReadOnly:   c.ReadOnly,
 		Hosts:      c.Hosts,
 		Secrets:    secretReferencesToGRPC(c.Secrets),
+		Configs:    configReferencesToGRPC(c.Configs),
 	}
 
 	if c.DNSConfig != nil {

+ 2 - 1
daemon/cluster/executor/backend.go

@@ -42,8 +42,9 @@ type Backend interface {
 	ContainerWaitWithContext(ctx context.Context, name string) error
 	ContainerRm(name string, config *types.ContainerRmConfig) error
 	ContainerKill(name string, sig uint64) error
-	SetContainerSecretStore(name string, store exec.SecretGetter) error
+	SetContainerDependencyStore(name string, store exec.DependencyGetter) error
 	SetContainerSecretReferences(name string, refs []*swarmtypes.SecretReference) error
+	SetContainerConfigReferences(name string, refs []*swarmtypes.ConfigReference) error
 	SystemInfo() (*types.Info, error)
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
 	Containers(config *types.ContainerListOptions) ([]*types.Container, error)

+ 15 - 10
daemon/cluster/executor/container/adapter.go

@@ -33,21 +33,21 @@ import (
 // are mostly naked calls to the client API, seeded with information from
 // containerConfig.
 type containerAdapter struct {
-	backend   executorpkg.Backend
-	container *containerConfig
-	secrets   exec.SecretGetter
+	backend      executorpkg.Backend
+	container    *containerConfig
+	dependencies exec.DependencyGetter
 }
 
-func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
+func newContainerAdapter(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*containerAdapter, error) {
 	ctnr, err := newContainerConfig(task)
 	if err != nil {
 		return nil, err
 	}
 
 	return &containerAdapter{
-		container: ctnr,
-		backend:   b,
-		secrets:   secrets,
+		container:    ctnr,
+		backend:      b,
+		dependencies: dependencies,
 	}, nil
 }
 
@@ -243,13 +243,18 @@ func (c *containerAdapter) create(ctx context.Context) error {
 		return errors.New("unable to get container from task spec")
 	}
 
+	if err := c.backend.SetContainerDependencyStore(cr.ID, c.dependencies); err != nil {
+		return err
+	}
+
 	// configure secrets
-	if err := c.backend.SetContainerSecretStore(cr.ID, c.secrets); err != nil {
+	secretRefs := convert.SecretReferencesFromGRPC(container.Secrets)
+	if err := c.backend.SetContainerSecretReferences(cr.ID, secretRefs); err != nil {
 		return err
 	}
 
-	refs := convert.SecretReferencesFromGRPC(container.Secrets)
-	if err := c.backend.SetContainerSecretReferences(cr.ID, refs); err != nil {
+	configRefs := convert.ConfigReferencesFromGRPC(container.Configs)
+	if err := c.backend.SetContainerConfigReferences(cr.ID, configRefs); err != nil {
 		return err
 	}
 

+ 2 - 2
daemon/cluster/executor/container/attachment.go

@@ -20,8 +20,8 @@ type networkAttacherController struct {
 	closed  chan struct{}
 }
 
-func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*networkAttacherController, error) {
-	adapter, err := newContainerAdapter(b, task, secrets)
+func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*networkAttacherController, error) {
+	adapter, err := newContainerAdapter(b, task, dependencies)
 	if err != nil {
 		return nil, err
 	}

+ 2 - 2
daemon/cluster/executor/container/controller.go

@@ -39,8 +39,8 @@ type controller struct {
 var _ exec.Controller = &controller{}
 
 // NewController returns a docker exec runner for the provided task.
-func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*controller, error) {
-	adapter, err := newContainerAdapter(b, task, secrets)
+func newController(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*controller, error) {
+	adapter, err := newContainerAdapter(b, task, dependencies)
 	if err != nil {
 		return nil, err
 	}

+ 14 - 8
daemon/cluster/executor/container/executor.go

@@ -14,23 +14,23 @@ import (
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
 	networktypes "github.com/docker/libnetwork/types"
+	"github.com/docker/swarmkit/agent"
 	"github.com/docker/swarmkit/agent/exec"
-	"github.com/docker/swarmkit/agent/secrets"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/api/naming"
 	"golang.org/x/net/context"
 )
 
 type executor struct {
-	backend executorpkg.Backend
-	secrets exec.SecretsManager
+	backend      executorpkg.Backend
+	dependencies exec.DependencyManager
 }
 
 // NewExecutor returns an executor from the docker client.
 func NewExecutor(b executorpkg.Backend) exec.Executor {
 	return &executor{
-		backend: b,
-		secrets: secrets.NewManager(),
+		backend:      b,
+		dependencies: agent.NewDependencyManager(),
 	}
 }
 
@@ -162,8 +162,10 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
 
 // Controller returns a docker container runner.
 func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
+	dependencyGetter := agent.Restrict(e.dependencies, t)
+
 	if t.Spec.GetAttachment() != nil {
-		return newNetworkAttacherController(e.backend, t, e.secrets)
+		return newNetworkAttacherController(e.backend, t, dependencyGetter)
 	}
 
 	var ctlr exec.Controller
@@ -188,7 +190,7 @@ func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
 			return ctlr, fmt.Errorf("unsupported runtime type: %q", r.Generic.Kind)
 		}
 	case *api.TaskSpec_Container:
-		c, err := newController(e.backend, t, secrets.Restrict(e.secrets, t))
+		c, err := newController(e.backend, t, dependencyGetter)
 		if err != nil {
 			return ctlr, err
 		}
@@ -218,7 +220,11 @@ func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error {
 }
 
 func (e *executor) Secrets() exec.SecretsManager {
-	return e.secrets
+	return e.dependencies.Secrets()
+}
+
+func (e *executor) Configs() exec.ConfigsManager {
+	return e.dependencies.Configs()
 }
 
 type sortedPlugins []api.PluginDescription

+ 16 - 0
daemon/cluster/filters.go

@@ -103,3 +103,19 @@ func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Fi
 		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
 	}, nil
 }
+
+func newListConfigsFilters(filter filters.Args) (*swarmapi.ListConfigsRequest_Filters, error) {
+	accepted := map[string]bool{
+		"name":  true,
+		"id":    true,
+		"label": true,
+	}
+	if err := filter.Validate(accepted); err != nil {
+		return nil, err
+	}
+	return &swarmapi.ListConfigsRequest_Filters{
+		NamePrefixes: filter.Get("name"),
+		IDPrefixes:   filter.Get("id"),
+		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
+	}, nil
+}

+ 45 - 0
daemon/cluster/filters_test.go

@@ -55,3 +55,48 @@ func TestNewListSecretsFilters(t *testing.T) {
 		}
 	}
 }
+
+func TestNewListConfigsFilters(t *testing.T) {
+	validNameFilter := filters.NewArgs()
+	validNameFilter.Add("name", "test_name")
+
+	validIDFilter := filters.NewArgs()
+	validIDFilter.Add("id", "7c9009d6720f6de3b492f5")
+
+	validLabelFilter := filters.NewArgs()
+	validLabelFilter.Add("label", "type=test")
+	validLabelFilter.Add("label", "storage=ssd")
+	validLabelFilter.Add("label", "memory")
+
+	validAllFilter := filters.NewArgs()
+	validAllFilter.Add("name", "nodeName")
+	validAllFilter.Add("id", "7c9009d6720f6de3b492f5")
+	validAllFilter.Add("label", "type=test")
+	validAllFilter.Add("label", "memory")
+
+	validFilters := []filters.Args{
+		validNameFilter,
+		validIDFilter,
+		validLabelFilter,
+		validAllFilter,
+	}
+
+	invalidTypeFilter := filters.NewArgs()
+	invalidTypeFilter.Add("nonexist", "aaaa")
+
+	invalidFilters := []filters.Args{
+		invalidTypeFilter,
+	}
+
+	for _, filter := range validFilters {
+		if _, err := newListConfigsFilters(filter); err != nil {
+			t.Fatalf("Should get no error, got %v", err)
+		}
+	}
+
+	for _, filter := range invalidFilters {
+		if _, err := newListConfigsFilters(filter); err == nil {
+			t.Fatalf("Should get an error for filter %s, while got nil", filter)
+		}
+	}
+}

+ 36 - 0
daemon/cluster/helpers.go

@@ -174,6 +174,42 @@ func getSecret(ctx context.Context, c swarmapi.ControlClient, input string) (*sw
 	return rl.Secrets[0], nil
 }
 
+func getConfig(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Config, error) {
+	// attempt to lookup config by full ID
+	if rg, err := c.GetConfig(ctx, &swarmapi.GetConfigRequest{ConfigID: input}); err == nil {
+		return rg.Config, nil
+	}
+
+	// If any error (including NotFound), ListConfigs to match via full name.
+	rl, err := c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
+		Filters: &swarmapi.ListConfigsRequest_Filters{
+			Names: []string{input},
+		},
+	})
+	if err != nil || len(rl.Configs) == 0 {
+		// If any error or 0 result, ListConfigs to match via ID prefix.
+		rl, err = c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
+			Filters: &swarmapi.ListConfigsRequest_Filters{
+				IDPrefixes: []string{input},
+			},
+		})
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rl.Configs) == 0 {
+		err := fmt.Errorf("config %s not found", input)
+		return nil, errors.NewRequestNotFoundError(err)
+	}
+
+	if l := len(rl.Configs); l > 1 {
+		return nil, fmt.Errorf("config %s is ambiguous (%d matches found)", input, l)
+	}
+
+	return rl.Configs[0], nil
+}
+
 func getNetwork(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Network, error) {
 	// GetNetwork to match via full ID.
 	if rg, err := c.GetNetwork(ctx, &swarmapi.GetNetworkRequest{NetworkID: input}); err == nil {

+ 23 - 0
daemon/configs.go

@@ -0,0 +1,23 @@
+package daemon
+
+import (
+	"github.com/Sirupsen/logrus"
+	swarmtypes "github.com/docker/docker/api/types/swarm"
+)
+
+// SetContainerConfigReferences sets the container config references needed
+func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtypes.ConfigReference) error {
+	if !configsSupported() && len(refs) > 0 {
+		logrus.Warn("configs are not supported on this platform")
+		return nil
+	}
+
+	c, err := daemon.GetContainer(name)
+	if err != nil {
+		return err
+	}
+
+	c.ConfigReferences = refs
+
+	return nil
+}

+ 7 - 0
daemon/configs_linux.go

@@ -0,0 +1,7 @@
+// +build linux
+
+package daemon
+
+func configsSupported() bool {
+	return true
+}

+ 7 - 0
daemon/configs_unsupported.go

@@ -0,0 +1,7 @@
+// +build !linux
+
+package daemon
+
+func configsSupported() bool {
+	return false
+}

+ 82 - 12
daemon/container_operations_unix.go

@@ -145,6 +145,13 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 	localMountPath := c.SecretMountPath()
 	logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
 
+	// retrieve possible remapped range start for root UID, GID
+	rootUID, rootGID := daemon.GetRemappedUIDGID()
+	// create tmpfs
+	if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil {
+		return errors.Wrap(err, "error creating secret local mount path")
+	}
+
 	defer func() {
 		if setupErr != nil {
 			// cleanup
@@ -156,25 +163,20 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 		}
 	}()
 
-	// retrieve possible remapped range start for root UID, GID
-	rootUID, rootGID := daemon.GetRemappedUIDGID()
-	// create tmpfs
-	if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil {
-		return errors.Wrap(err, "error creating secret local mount path")
-	}
 	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootUID, rootGID)
 	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
 		return errors.Wrap(err, "unable to setup secret mount")
 	}
 
-	for _, s := range c.SecretReferences {
-		if c.SecretStore == nil {
-			return fmt.Errorf("secret store is not initialized")
-		}
+	if c.DependencyStore == nil {
+		return fmt.Errorf("secret store is not initialized")
+	}
 
+	for _, s := range c.SecretReferences {
 		// TODO (ehazlett): use type switch when more are supported
 		if s.File == nil {
-			return fmt.Errorf("secret target type is not a file target")
+			logrus.Error("secret target type is not a file target")
+			continue
 		}
 
 		// secrets are created in the SecretMountPath on the host, at a
@@ -188,7 +190,7 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 			"name": s.File.Name,
 			"path": fPath,
 		}).Debug("injecting secret")
-		secret := c.SecretStore.Get(s.SecretID)
+		secret := c.DependencyStore.Secrets().Get(s.SecretID)
 		if secret == nil {
 			return fmt.Errorf("unable to get secret from secret store")
 		}
@@ -220,6 +222,74 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 	return nil
 }
 
+func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
+	if len(c.ConfigReferences) == 0 {
+		return nil
+	}
+
+	localPath := c.ConfigsDirPath()
+	logrus.Debugf("configs: setting up config dir: %s", localPath)
+
+	// retrieve possible remapped range start for root UID, GID
+	rootUID, rootGID := daemon.GetRemappedUIDGID()
+	// create tmpfs
+	if err := idtools.MkdirAllAs(localPath, 0700, rootUID, rootGID); err != nil {
+		return errors.Wrap(err, "error creating config dir")
+	}
+
+	defer func() {
+		if setupErr != nil {
+			if err := os.RemoveAll(localPath); err != nil {
+				logrus.Errorf("error cleaning up config dir: %s", err)
+			}
+		}
+	}()
+
+	if c.DependencyStore == nil {
+		return fmt.Errorf("config store is not initialized")
+	}
+
+	for _, configRef := range c.ConfigReferences {
+		// TODO (ehazlett): use type switch when more are supported
+		if configRef.File == nil {
+			logrus.Error("config target type is not a file target")
+			continue
+		}
+
+		fPath := c.ConfigFilePath(*configRef)
+
+		log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
+
+		if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil {
+			return errors.Wrap(err, "error creating config path")
+		}
+
+		log.Debug("injecting config")
+		config := c.DependencyStore.Configs().Get(configRef.ConfigID)
+		if config == nil {
+			return fmt.Errorf("unable to get config from config store")
+		}
+		if err := ioutil.WriteFile(fPath, config.Spec.Data, configRef.File.Mode); err != nil {
+			return errors.Wrap(err, "error injecting config")
+		}
+
+		uid, err := strconv.Atoi(configRef.File.UID)
+		if err != nil {
+			return err
+		}
+		gid, err := strconv.Atoi(configRef.File.GID)
+		if err != nil {
+			return err
+		}
+
+		if err := os.Chown(fPath, rootUID+uid, rootGID+gid); err != nil {
+			return errors.Wrap(err, "error setting ownership for config")
+		}
+	}
+
+	return nil
+}
+
 func killProcessDirectly(container *container.Container) error {
 	if _, err := container.WaitStop(10 * time.Second); err != nil {
 		// Ensure that we don't kill ourselves

+ 17 - 0
daemon/dependency.go

@@ -0,0 +1,17 @@
+package daemon
+
+import (
+	"github.com/docker/swarmkit/agent/exec"
+)
+
+// SetContainerDependencyStore sets the dependency store backend for the container
+func (daemon *Daemon) SetContainerDependencyStore(name string, store exec.DependencyGetter) error {
+	c, err := daemon.GetContainer(name)
+	if err != nil {
+		return err
+	}
+
+	c.DependencyStore = store
+
+	return nil
+}

+ 6 - 0
daemon/oci_linux.go

@@ -737,6 +737,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		return nil, err
 	}
 
+	if err := daemon.setupConfigDir(c); err != nil {
+		return nil, err
+	}
+
 	ms, err := daemon.setupMounts(c)
 	if err != nil {
 		return nil, err
@@ -754,6 +758,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		ms = append(ms, m...)
 	}
 
+	ms = append(ms, c.ConfigMounts()...)
+
 	sort.Sort(mounts(ms))
 	if err := setMounts(daemon, &s, c, ms); err != nil {
 		return nil, fmt.Errorf("linux mounts: %v", err)

+ 0 - 13
daemon/secrets.go

@@ -3,21 +3,8 @@ package daemon
 import (
 	"github.com/Sirupsen/logrus"
 	swarmtypes "github.com/docker/docker/api/types/swarm"
-	"github.com/docker/swarmkit/agent/exec"
 )
 
-// SetContainerSecretStore sets the secret store backend for the container
-func (daemon *Daemon) SetContainerSecretStore(name string, store exec.SecretGetter) error {
-	c, err := daemon.GetContainer(name)
-	if err != nil {
-		return err
-	}
-
-	c.SecretStore = store
-
-	return nil
-}
-
 // SetContainerSecretReferences sets the container secret references needed
 func (daemon *Daemon) SetContainerSecretReferences(name string, refs []*swarmtypes.SecretReference) error {
 	if !secretsSupported() && len(refs) > 0 {

+ 9 - 0
docs/reference/commandline/cli.md

@@ -167,12 +167,20 @@ property is not set, the client falls back to the default table
 format. For a list of supported formatting directives, see
 [**Formatting** section in the `docker secret ls` documentation](secret_ls.md)
 
+
 The property `nodesFormat` specifies the default format for `docker node ls` output.
 When the `--format` flag is not provided with the `docker node ls` command,
 Docker's client uses the value of `nodesFormat`. If the value of `nodesFormat` is not set,
 the client uses the default table format. For a list of supported formatting
 directives, see the [**Formatting** section in the `docker node ls` documentation](node_ls.md)
 
+The property `configFormat` specifies the default format for `docker
+config ls` output. When the `--format` flag is not provided with the
+`docker config ls` command, Docker's client uses this property. If this
+property is not set, the client falls back to the default table
+format. For a list of supported formatting directives, see
+[**Formatting** section in the `docker config ls` documentation](config_ls.md)
+
 The property `credsStore` specifies an external binary to serve as the default
 credential store. When this property is set, `docker login` will attempt to
 store credentials in the binary specified by `docker-credential-<value>` which
@@ -218,6 +226,7 @@ Following is a sample `config.json` file:
   "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
   "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
   "secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
+  "configFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
   "serviceInspectFormat": "pretty",
   "nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}",
   "detachKeys": "ctrl-e,e",

+ 2 - 2
hack/dockerfile/binaries-commits

@@ -11,5 +11,5 @@ VNDR_COMMIT=c56e082291115e369f77601f9c071dd0b87c7120
 BINDATA_COMMIT=a0ff2567cfb70903282db057e799fd826784d41d
 
 # CLI
-DOCKERCLI_REPO=https://github.com/docker/cli
-DOCKERCLI_COMMIT=0f6f1eafe31c4beceba31490017878b80b609331
+DOCKERCLI_REPO=https://github.com/aaronlehmann/cli
+DOCKERCLI_COMMIT=0e2b9d30814eaecd6518bfb112bb4adef0265d4b

+ 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)
+}

+ 98 - 0
opts/config.go

@@ -0,0 +1,98 @@
+package opts
+
+import (
+	"encoding/csv"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+
+	swarmtypes "github.com/docker/docker/api/types/swarm"
+)
+
+// ConfigOpt is a Value type for parsing configs
+type ConfigOpt struct {
+	values []*swarmtypes.ConfigReference
+}
+
+// Set a new config value
+func (o *ConfigOpt) Set(value string) error {
+	csvReader := csv.NewReader(strings.NewReader(value))
+	fields, err := csvReader.Read()
+	if err != nil {
+		return err
+	}
+
+	options := &swarmtypes.ConfigReference{
+		File: &swarmtypes.ConfigReferenceFileTarget{
+			UID:  "0",
+			GID:  "0",
+			Mode: 0444,
+		},
+	}
+
+	// support a simple syntax of --config foo
+	if len(fields) == 1 {
+		options.File.Name = fields[0]
+		options.ConfigName = fields[0]
+		o.values = append(o.values, options)
+		return nil
+	}
+
+	for _, field := range fields {
+		parts := strings.SplitN(field, "=", 2)
+		key := strings.ToLower(parts[0])
+
+		if len(parts) != 2 {
+			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
+		}
+
+		value := parts[1]
+		switch key {
+		case "source", "src":
+			options.ConfigName = value
+		case "target":
+			options.File.Name = value
+		case "uid":
+			options.File.UID = value
+		case "gid":
+			options.File.GID = value
+		case "mode":
+			m, err := strconv.ParseUint(value, 0, 32)
+			if err != nil {
+				return fmt.Errorf("invalid mode specified: %v", err)
+			}
+
+			options.File.Mode = os.FileMode(m)
+		default:
+			return fmt.Errorf("invalid field in config request: %s", key)
+		}
+	}
+
+	if options.ConfigName == "" {
+		return fmt.Errorf("source is required")
+	}
+
+	o.values = append(o.values, options)
+	return nil
+}
+
+// Type returns the type of this option
+func (o *ConfigOpt) Type() string {
+	return "config"
+}
+
+// String returns a string repr of this option
+func (o *ConfigOpt) String() string {
+	configs := []string{}
+	for _, config := range o.values {
+		repr := fmt.Sprintf("%s -> %s", config.ConfigName, config.File.Name)
+		configs = append(configs, repr)
+	}
+	return strings.Join(configs, ", ")
+}
+
+// Value returns the config requests
+func (o *ConfigOpt) Value() []*swarmtypes.ConfigReference {
+	return o.values
+}