Browse Source

Merge pull request #38632 from dperny/gmsa-support

Add support for GMSA CredentialSpecs from Swarmkit configs
Brian Goff 6 years ago
parent
commit
cbb885b07a

+ 2 - 26
api/server/router/swarm/cluster_routes.go

@@ -213,19 +213,7 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
 		if versions.LessThan(cliVersion, "1.30") {
 			queryRegistry = true
 		}
-		if versions.LessThan(cliVersion, "1.40") {
-			if service.TaskTemplate.ContainerSpec != nil {
-				// Sysctls for docker swarm services weren't supported before
-				// API version 1.40
-				service.TaskTemplate.ContainerSpec.Sysctls = nil
-			}
-
-			if service.TaskTemplate.Placement != nil {
-				// MaxReplicas for docker swarm services weren't supported before
-				// API version 1.40
-				service.TaskTemplate.Placement.MaxReplicas = 0
-			}
-		}
+		adjustForAPIVersion(cliVersion, &service)
 	}
 
 	resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry)
@@ -265,19 +253,7 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
 		if versions.LessThan(cliVersion, "1.30") {
 			queryRegistry = true
 		}
-		if versions.LessThan(cliVersion, "1.40") {
-			if service.TaskTemplate.ContainerSpec != nil {
-				// Sysctls for docker swarm services weren't supported before
-				// API version 1.40
-				service.TaskTemplate.ContainerSpec.Sysctls = nil
-			}
-
-			if service.TaskTemplate.Placement != nil {
-				// MaxReplicas for docker swarm services weren't supported before
-				// API version 1.40
-				service.TaskTemplate.Placement.MaxReplicas = 0
-			}
-		}
+		adjustForAPIVersion(cliVersion, &service)
 	}
 
 	resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry)

+ 32 - 0
api/server/router/swarm/helpers.go

@@ -9,6 +9,8 @@ import (
 	"github.com/docker/docker/api/server/httputils"
 	basictypes "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/api/types/versions"
 )
 
 // swarmLogs takes an http response, request, and selector, and writes the logs
@@ -64,3 +66,33 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w io.Writer, r *http.Reque
 	httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty)
 	return nil
 }
+
+// adjustForAPIVersion takes a version and service spec and removes fields to
+// make the spec compatible with the specified version.
+func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
+	if cliVersion == "" {
+		return
+	}
+	if versions.LessThan(cliVersion, "1.40") {
+		if service.TaskTemplate.ContainerSpec != nil {
+			// Sysctls for docker swarm services weren't supported before
+			// API version 1.40
+			service.TaskTemplate.ContainerSpec.Sysctls = nil
+
+			if service.TaskTemplate.ContainerSpec.Privileges != nil && service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec != nil {
+				// Support for setting credential-spec through configs was added in API 1.40
+				service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config = ""
+			}
+			for _, config := range service.TaskTemplate.ContainerSpec.Configs {
+				// support for the Runtime target was added in API 1.40
+				config.Runtime = nil
+			}
+		}
+
+		if service.TaskTemplate.Placement != nil {
+			// MaxReplicas for docker swarm services weren't supported before
+			// API version 1.40
+			service.TaskTemplate.Placement.MaxReplicas = 0
+		}
+	}
+}

+ 87 - 0
api/server/router/swarm/helpers_test.go

@@ -0,0 +1,87 @@
+package swarm // import "github.com/docker/docker/api/server/router/swarm"
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/docker/docker/api/types/swarm"
+)
+
+func TestAdjustForAPIVersion(t *testing.T) {
+	var (
+		expectedSysctls = map[string]string{"foo": "bar"}
+	)
+	// testing the negative -- does this leave everything else alone? -- is
+	// prohibitively time-consuming to write, because it would need an object
+	// with literally every field filled in.
+	spec := &swarm.ServiceSpec{
+		TaskTemplate: swarm.TaskSpec{
+			ContainerSpec: &swarm.ContainerSpec{
+				Sysctls: expectedSysctls,
+				Privileges: &swarm.Privileges{
+					CredentialSpec: &swarm.CredentialSpec{
+						Config: "someconfig",
+					},
+				},
+				Configs: []*swarm.ConfigReference{
+					{
+						File: &swarm.ConfigReferenceFileTarget{
+							Name: "foo",
+							UID:  "bar",
+							GID:  "baz",
+						},
+						ConfigID:   "configFile",
+						ConfigName: "configFile",
+					},
+					{
+						Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
+						ConfigID:   "configRuntime",
+						ConfigName: "configRuntime",
+					},
+				},
+			},
+			Placement: &swarm.Placement{
+				MaxReplicas: 222,
+			},
+		},
+	}
+
+	// first, does calling this with a later version correctly NOT strip
+	// fields? do the later version first, so we can reuse this spec in the
+	// next test.
+	adjustForAPIVersion("1.40", spec)
+	if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls) {
+		t.Error("Sysctls was stripped from spec")
+	}
+
+	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "someconfig" {
+		t.Error("CredentialSpec.Config field was stripped from spec")
+	}
+
+	if spec.TaskTemplate.ContainerSpec.Configs[1].Runtime == nil {
+		t.Error("ConfigReferenceRuntimeTarget was stripped from spec")
+	}
+
+	if spec.TaskTemplate.Placement.MaxReplicas != 222 {
+		t.Error("MaxReplicas was stripped from spec")
+	}
+
+	// next, does calling this with an earlier version correctly strip fields?
+	adjustForAPIVersion("1.29", spec)
+	if spec.TaskTemplate.ContainerSpec.Sysctls != nil {
+		t.Error("Sysctls was not stripped from spec")
+	}
+
+	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "" {
+		t.Error("CredentialSpec.Config field was not stripped from spec")
+	}
+
+	if spec.TaskTemplate.ContainerSpec.Configs[1].Runtime != nil {
+		t.Error("ConfigReferenceRuntimeTarget was not stripped from spec")
+	}
+
+	if spec.TaskTemplate.Placement.MaxReplicas != 0 {
+		t.Error("MaxReplicas was not stripped from spec")
+	}
+
+}

+ 28 - 3
api/swagger.yaml

@@ -2623,8 +2623,20 @@ definitions:
                 type: "object"
                 description: "CredentialSpec for managed service account (Windows only)"
                 properties:
+                  Config:
+                    type: "string"
+                    example: "0bt9dmxjvjiqermk6xrop3ekq"
+                    description: |
+                      Load credential spec from a Swarm Config with the given ID.
+                      The specified config must also be present in the Configs field with the Runtime property set.
+
+                      <p><br /></p>
+
+
+                      > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, and `CredentialSpec.Config` are mutually exclusive.
                   File:
                     type: "string"
+                    example: "spec.json"
                     description: |
                       Load credential spec from this file. The file is read by the daemon, and must be present in the
                       `CredentialSpecs` subdirectory in the docker data directory, which defaults to
@@ -2634,7 +2646,7 @@ definitions:
 
                       <p><br /></p>
 
-                      > **Note**: `CredentialSpec.File` and `CredentialSpec.Registry` are mutually exclusive.
+                      > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, and `CredentialSpec.Config` are mutually exclusive.
                   Registry:
                     type: "string"
                     description: |
@@ -2646,7 +2658,7 @@ definitions:
                       <p><br /></p>
 
 
-                      > **Note**: `CredentialSpec.File` and `CredentialSpec.Registry` are mutually exclusive.
+                      > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, and `CredentialSpec.Config` are mutually exclusive.
               SELinuxContext:
                 type: "object"
                 description: "SELinux labels of the container"
@@ -2757,7 +2769,12 @@ definitions:
               type: "object"
               properties:
                 File:
-                  description: "File represents a specific target that is backed by a file."
+                  description: |
+                    File represents a specific target that is backed by a file.
+
+                    <p><br /><p>
+
+                    > **Note**: `Configs.File` and `Configs.Runtime` are mutually exclusive
                   type: "object"
                   properties:
                     Name:
@@ -2773,6 +2790,14 @@ definitions:
                       description: "Mode represents the FileMode of the file."
                       type: "integer"
                       format: "uint32"
+                Runtime:
+                  description: |
+                    Runtime represents a target that is not mounted into the container but is used by the task
+
+                    <p><br /><p>
+
+                    > **Note**: `Configs.File` and `Configs.Runtime` are mutually exclusive
+                  type: "object"
                 ConfigID:
                   description: "ConfigID represents the ID of the specific config that we're referencing."
                   type: "string"

+ 6 - 1
api/types/swarm/config.go

@@ -27,9 +27,14 @@ type ConfigReferenceFileTarget struct {
 	Mode os.FileMode
 }
 
+// ConfigReferenceRuntimeTarget is a target for a config specifying that it
+// isn't mounted into the container but instead has some other purpose.
+type ConfigReferenceRuntimeTarget struct{}
+
 // ConfigReference is a reference to a config in swarm
 type ConfigReference struct {
-	File       *ConfigReferenceFileTarget
+	File       *ConfigReferenceFileTarget    `json:",omitempty"`
+	Runtime    *ConfigReferenceRuntimeTarget `json:",omitempty"`
 	ConfigID   string
 	ConfigName string
 }

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

@@ -33,6 +33,7 @@ type SELinuxContext struct {
 
 // CredentialSpec for managed service account (Windows only)
 type CredentialSpec struct {
+	Config   string
 	File     string
 	Registry string
 }

+ 98 - 38
daemon/cluster/convert/container.go

@@ -1,7 +1,6 @@
 package convert // import "github.com/docker/docker/daemon/cluster/convert"
 
 import (
-	"errors"
 	"fmt"
 	"strings"
 
@@ -10,6 +9,7 @@ import (
 	types "github.com/docker/docker/api/types/swarm"
 	swarmapi "github.com/docker/swarmkit/api"
 	gogotypes "github.com/gogo/protobuf/types"
+	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
 
@@ -52,13 +52,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
 		containerSpec.Privileges = &types.Privileges{}
 
 		if c.Privileges.CredentialSpec != nil {
-			containerSpec.Privileges.CredentialSpec = &types.CredentialSpec{}
-			switch c.Privileges.CredentialSpec.Source.(type) {
-			case *swarmapi.Privileges_CredentialSpec_File:
-				containerSpec.Privileges.CredentialSpec.File = c.Privileges.CredentialSpec.GetFile()
-			case *swarmapi.Privileges_CredentialSpec_Registry:
-				containerSpec.Privileges.CredentialSpec.Registry = c.Privileges.CredentialSpec.GetRegistry()
-			}
+			containerSpec.Privileges.CredentialSpec = credentialSpecFromGRPC(c.Privileges.CredentialSpec)
 		}
 
 		if c.Privileges.SELinuxContext != nil {
@@ -184,14 +178,26 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef
 	return refs
 }
 
-func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigReference {
+func configReferencesToGRPC(sr []*types.ConfigReference) ([]*swarmapi.ConfigReference, error) {
 	refs := make([]*swarmapi.ConfigReference, 0, len(sr))
 	for _, s := range sr {
 		ref := &swarmapi.ConfigReference{
 			ConfigID:   s.ConfigID,
 			ConfigName: s.ConfigName,
 		}
-		if s.File != nil {
+		switch {
+		case s.Runtime == nil && s.File == nil:
+			return nil, errors.New("either File or Runtime should be set")
+		case s.Runtime != nil && s.File != nil:
+			return nil, errors.New("cannot specify both File and Runtime")
+		case s.Runtime != nil:
+			// Runtime target was added in API v1.40 and takes precedence over
+			// File target. However, File and Runtime targets are mutually exclusive,
+			// so we should never have both.
+			ref.Target = &swarmapi.ConfigReference_Runtime{
+				Runtime: &swarmapi.RuntimeTarget{},
+			}
+		case s.File != nil:
 			ref.Target = &swarmapi.ConfigReference_File{
 				File: &swarmapi.FileTarget{
 					Name: s.File.Name,
@@ -205,28 +211,32 @@ func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigRefer
 		refs = append(refs, ref)
 	}
 
-	return refs
+	return refs, nil
 }
 
 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
+
+		r := &types.ConfigReference{
+			ConfigID:   s.ConfigID,
+			ConfigName: s.ConfigName,
 		}
-		refs = append(refs, &types.ConfigReference{
-			File: &types.ConfigReferenceFileTarget{
+		if target := s.GetRuntime(); target != nil {
+			r.Runtime = &types.ConfigReferenceRuntimeTarget{}
+		} else if target := s.GetFile(); target != nil {
+			r.File = &types.ConfigReferenceFileTarget{
 				Name: target.Name,
 				UID:  target.UID,
 				GID:  target.GID,
 				Mode: target.Mode,
-			},
-			ConfigID:   s.ConfigID,
-			ConfigName: s.ConfigName,
-		})
+			}
+		} else {
+			// not a file target
+			logrus.Warnf("config target not known: config=%s", s.ConfigID)
+			continue
+		}
+		refs = append(refs, r)
 	}
 
 	return refs
@@ -249,7 +259,6 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		ReadOnly:   c.ReadOnly,
 		Hosts:      c.Hosts,
 		Secrets:    secretReferencesToGRPC(c.Secrets),
-		Configs:    configReferencesToGRPC(c.Configs),
 		Isolation:  isolationToGRPC(c.Isolation),
 		Init:       initToGRPC(c.Init),
 		Sysctls:    c.Sysctls,
@@ -272,22 +281,11 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		containerSpec.Privileges = &swarmapi.Privileges{}
 
 		if c.Privileges.CredentialSpec != nil {
-			containerSpec.Privileges.CredentialSpec = &swarmapi.Privileges_CredentialSpec{}
-
-			if c.Privileges.CredentialSpec.File != "" && c.Privileges.CredentialSpec.Registry != "" {
-				return nil, errors.New("cannot specify both \"file\" and \"registry\" credential specs")
-			}
-			if c.Privileges.CredentialSpec.File != "" {
-				containerSpec.Privileges.CredentialSpec.Source = &swarmapi.Privileges_CredentialSpec_File{
-					File: c.Privileges.CredentialSpec.File,
-				}
-			} else if c.Privileges.CredentialSpec.Registry != "" {
-				containerSpec.Privileges.CredentialSpec.Source = &swarmapi.Privileges_CredentialSpec_Registry{
-					Registry: c.Privileges.CredentialSpec.Registry,
-				}
-			} else {
-				return nil, errors.New("must either provide \"file\" or \"registry\" for credential spec")
+			cs, err := credentialSpecToGRPC(c.Privileges.CredentialSpec)
+			if err != nil {
+				return nil, errors.Wrap(err, "invalid CredentialSpec")
 			}
+			containerSpec.Privileges.CredentialSpec = cs
 		}
 
 		if c.Privileges.SELinuxContext != nil {
@@ -301,6 +299,14 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		}
 	}
 
+	if c.Configs != nil {
+		configs, err := configReferencesToGRPC(c.Configs)
+		if err != nil {
+			return nil, errors.Wrap(err, "invalid Config")
+		}
+		containerSpec.Configs = configs
+	}
+
 	// Mounts
 	for _, m := range c.Mounts {
 		mount := swarmapi.Mount{
@@ -359,6 +365,60 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 	return containerSpec, nil
 }
 
+func credentialSpecFromGRPC(c *swarmapi.Privileges_CredentialSpec) *types.CredentialSpec {
+	cs := &types.CredentialSpec{}
+	switch c.Source.(type) {
+	case *swarmapi.Privileges_CredentialSpec_Config:
+		cs.Config = c.GetConfig()
+	case *swarmapi.Privileges_CredentialSpec_File:
+		cs.File = c.GetFile()
+	case *swarmapi.Privileges_CredentialSpec_Registry:
+		cs.Registry = c.GetRegistry()
+	}
+	return cs
+}
+
+func credentialSpecToGRPC(c *types.CredentialSpec) (*swarmapi.Privileges_CredentialSpec, error) {
+	var opts []string
+
+	if c.Config != "" {
+		opts = append(opts, `"config"`)
+	}
+	if c.File != "" {
+		opts = append(opts, `"file"`)
+	}
+	if c.Registry != "" {
+		opts = append(opts, `"registry"`)
+	}
+	l := len(opts)
+	switch {
+	case l == 0:
+		return nil, errors.New(`must either provide "file", "registry", or "config" for credential spec`)
+	case l == 2:
+		return nil, fmt.Errorf("cannot specify both %s and %s credential specs", opts[0], opts[1])
+	case l > 2:
+		return nil, fmt.Errorf("cannot specify both %s, and %s credential specs", strings.Join(opts[:l-1], ", "), opts[l-1])
+	}
+
+	spec := &swarmapi.Privileges_CredentialSpec{}
+	switch {
+	case c.Config != "":
+		spec.Source = &swarmapi.Privileges_CredentialSpec_Config{
+			Config: c.Config,
+		}
+	case c.File != "":
+		spec.Source = &swarmapi.Privileges_CredentialSpec_File{
+			File: c.File,
+		}
+	case c.Registry != "":
+		spec.Source = &swarmapi.Privileges_CredentialSpec_Registry{
+			Registry: c.Registry,
+		}
+	}
+
+	return spec, nil
+}
+
 func healthConfigFromGRPC(h *swarmapi.HealthConfig) *container.HealthConfig {
 	interval, _ := gogotypes.DurationFromProto(h.Interval)
 	timeout, _ := gogotypes.DurationFromProto(h.Timeout)

+ 305 - 0
daemon/cluster/convert/service_test.go

@@ -233,6 +233,167 @@ func TestServiceConvertFromGRPCIsolation(t *testing.T) {
 	}
 }
 
+func TestServiceConvertToGRPCCredentialSpec(t *testing.T) {
+	cases := []struct {
+		name        string
+		from        swarmtypes.CredentialSpec
+		to          swarmapi.Privileges_CredentialSpec
+		expectedErr string
+	}{
+		{
+			name:        "empty credential spec",
+			from:        swarmtypes.CredentialSpec{},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: must either provide "file", "registry", or "config" for credential spec`,
+		},
+		{
+			name: "config and file credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config: "0bt9dmxjvjiqermk6xrop3ekq",
+				File:   "spec.json",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "file" credential specs`,
+		},
+		{
+			name: "config and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "registry" credential specs`,
+		},
+		{
+			name: "file and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				File:     "spec.json",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "file" and "registry" credential specs`,
+		},
+		{
+			name: "config and file and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
+				File:     "spec.json",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config", "file", and "registry" credential specs`,
+		},
+		{
+			name: "config credential spec",
+			from: swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			},
+		},
+		{
+			name: "file credential spec",
+			from: swarmtypes.CredentialSpec{File: "foo.json"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
+			},
+		},
+		{
+			name: "registry credential spec",
+			from: swarmtypes.CredentialSpec{Registry: "testing"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
+			},
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			s := swarmtypes.ServiceSpec{
+				TaskTemplate: swarmtypes.TaskSpec{
+					ContainerSpec: &swarmtypes.ContainerSpec{
+						Privileges: &swarmtypes.Privileges{
+							CredentialSpec: &c.from,
+						},
+					},
+				},
+			}
+
+			res, err := ServiceSpecToGRPC(s)
+			if c.expectedErr != "" {
+				assert.Error(t, err, c.expectedErr)
+				return
+			}
+
+			assert.NilError(t, err)
+			v, ok := res.Task.Runtime.(*swarmapi.TaskSpec_Container)
+			if !ok {
+				t.Fatal("expected type swarmapi.TaskSpec_Container")
+			}
+			assert.DeepEqual(t, c.to, *v.Container.Privileges.CredentialSpec)
+		})
+	}
+}
+
+func TestServiceConvertFromGRPCCredentialSpec(t *testing.T) {
+	cases := []struct {
+		name string
+		from swarmapi.Privileges_CredentialSpec
+		to   *swarmtypes.CredentialSpec
+	}{
+		{
+			name: "empty credential spec",
+			from: swarmapi.Privileges_CredentialSpec{},
+			to:   &swarmtypes.CredentialSpec{},
+		},
+		{
+			name: "config credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			},
+			to: &swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+		},
+		{
+			name: "file credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
+			},
+			to: &swarmtypes.CredentialSpec{File: "foo.json"},
+		},
+		{
+			name: "registry credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
+			},
+			to: &swarmtypes.CredentialSpec{Registry: "testing"},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			gs := swarmapi.Service{
+				Spec: swarmapi.ServiceSpec{
+					Task: swarmapi.TaskSpec{
+						Runtime: &swarmapi.TaskSpec_Container{
+							Container: &swarmapi.ContainerSpec{
+								Privileges: &swarmapi.Privileges{
+									CredentialSpec: &tc.from,
+								},
+							},
+						},
+					},
+				},
+			}
+
+			svc, err := ServiceFromGRPC(gs)
+			assert.NilError(t, err)
+			assert.DeepEqual(t, svc.Spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec, tc.to)
+		})
+	}
+}
+
 func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) {
 	someid := "asfjkl"
 	s := swarmtypes.ServiceSpec{
@@ -306,3 +467,147 @@ func TestTaskConvertFromGRPCNetworkAttachment(t *testing.T) {
 		t.Fatalf("expected Runtime to be %v", swarmtypes.RuntimeNetworkAttachment)
 	}
 }
+
+// TestServiceConvertFromGRPCConfigs tests that converting config references
+// from GRPC is correct
+func TestServiceConvertFromGRPCConfigs(t *testing.T) {
+	cases := []struct {
+		name string
+		from *swarmapi.ConfigReference
+		to   *swarmtypes.ConfigReference
+	}{
+		{
+			name: "file",
+			from: &swarmapi.ConfigReference{
+				ConfigID:   "configFile",
+				ConfigName: "configFile",
+				Target: &swarmapi.ConfigReference_File{
+					// skip mode, if everything else here works mode will too. otherwise we'd need to import os.
+					File: &swarmapi.FileTarget{Name: "foo", UID: "bar", GID: "baz"},
+				},
+			},
+			to: &swarmtypes.ConfigReference{
+				ConfigID:   "configFile",
+				ConfigName: "configFile",
+				File:       &swarmtypes.ConfigReferenceFileTarget{Name: "foo", UID: "bar", GID: "baz"},
+			},
+		},
+		{
+			name: "runtime",
+			from: &swarmapi.ConfigReference{
+				ConfigID:   "configRuntime",
+				ConfigName: "configRuntime",
+				Target:     &swarmapi.ConfigReference_Runtime{Runtime: &swarmapi.RuntimeTarget{}},
+			},
+			to: &swarmtypes.ConfigReference{
+				ConfigID:   "configRuntime",
+				ConfigName: "configRuntime",
+				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			grpcService := swarmapi.Service{
+				Spec: swarmapi.ServiceSpec{
+					Task: swarmapi.TaskSpec{
+						Runtime: &swarmapi.TaskSpec_Container{
+							Container: &swarmapi.ContainerSpec{
+								Configs: []*swarmapi.ConfigReference{tc.from},
+							},
+						},
+					},
+				},
+			}
+
+			engineService, err := ServiceFromGRPC(grpcService)
+			assert.NilError(t, err)
+			assert.DeepEqual(t,
+				engineService.Spec.TaskTemplate.ContainerSpec.Configs[0],
+				tc.to,
+			)
+		})
+	}
+}
+
+// TestServiceConvertToGRPCConfigs tests that converting config references to
+// GRPC is correct
+func TestServiceConvertToGRPCConfigs(t *testing.T) {
+	cases := []struct {
+		name        string
+		from        *swarmtypes.ConfigReference
+		to          *swarmapi.ConfigReference
+		expectedErr string
+	}{
+		{
+			name: "file",
+			from: &swarmtypes.ConfigReference{
+				ConfigID:   "configFile",
+				ConfigName: "configFile",
+				File:       &swarmtypes.ConfigReferenceFileTarget{Name: "foo", UID: "bar", GID: "baz"},
+			},
+			to: &swarmapi.ConfigReference{
+				ConfigID:   "configFile",
+				ConfigName: "configFile",
+				Target: &swarmapi.ConfigReference_File{
+					// skip mode, if everything else here works mode will too. otherwise we'd need to import os.
+					File: &swarmapi.FileTarget{Name: "foo", UID: "bar", GID: "baz"},
+				},
+			},
+		},
+		{
+			name: "runtime",
+			from: &swarmtypes.ConfigReference{
+				ConfigID:   "configRuntime",
+				ConfigName: "configRuntime",
+				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
+			},
+			to: &swarmapi.ConfigReference{
+				ConfigID:   "configRuntime",
+				ConfigName: "configRuntime",
+				Target:     &swarmapi.ConfigReference_Runtime{Runtime: &swarmapi.RuntimeTarget{}},
+			},
+		},
+		{
+			name: "file and runtime",
+			from: &swarmtypes.ConfigReference{
+				ConfigID:   "fileAndRuntime",
+				ConfigName: "fileAndRuntime",
+				File:       &swarmtypes.ConfigReferenceFileTarget{},
+				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
+			},
+			expectedErr: "invalid Config: cannot specify both File and Runtime",
+		},
+		{
+			name: "none",
+			from: &swarmtypes.ConfigReference{
+				ConfigID:   "none",
+				ConfigName: "none",
+			},
+			expectedErr: "invalid Config: either File or Runtime should be set",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			engineServiceSpec := swarmtypes.ServiceSpec{
+				TaskTemplate: swarmtypes.TaskSpec{
+					ContainerSpec: &swarmtypes.ContainerSpec{
+						Configs: []*swarmtypes.ConfigReference{tc.from},
+					},
+				},
+			}
+
+			grpcServiceSpec, err := ServiceSpecToGRPC(engineServiceSpec)
+			if tc.expectedErr != "" {
+				assert.Error(t, err, tc.expectedErr)
+				return
+			}
+
+			assert.NilError(t, err)
+			taskRuntime := grpcServiceSpec.Task.Runtime.(*swarmapi.TaskSpec_Container)
+			assert.DeepEqual(t, taskRuntime.Container.Configs[0], tc.to)
+		})
+	}
+}

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

@@ -651,6 +651,8 @@ func (c *containerConfig) applyPrivileges(hc *enginecontainer.HostConfig) {
 			hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=file://"+credentials.GetFile())
 		case *api.Privileges_CredentialSpec_Registry:
 			hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=registry://"+credentials.GetRegistry())
+		case *api.Privileges_CredentialSpec_Config:
+			hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=config://"+credentials.GetConfig())
 		}
 	}
 

+ 53 - 0
daemon/cluster/executor/container/container_test.go

@@ -80,3 +80,56 @@ func TestContainerLabels(t *testing.T) {
 	labels := c.labels()
 	assert.DeepEqual(t, expected, labels)
 }
+
+func TestCredentialSpecConversion(t *testing.T) {
+	cases := []struct {
+		name string
+		from swarmapi.Privileges_CredentialSpec
+		to   []string
+	}{
+		{
+			name: "none",
+			from: swarmapi.Privileges_CredentialSpec{},
+			to:   nil,
+		},
+		{
+			name: "config",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			},
+			to: []string{"credentialspec=config://0bt9dmxjvjiqermk6xrop3ekq"},
+		},
+		{
+			name: "file",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
+			},
+			to: []string{"credentialspec=file://foo.json"},
+		},
+		{
+			name: "registry",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
+			},
+			to: []string{"credentialspec=registry://testing"},
+		},
+	}
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			task := swarmapi.Task{
+				Spec: swarmapi.TaskSpec{
+					Runtime: &swarmapi.TaskSpec_Container{
+						Container: &swarmapi.ContainerSpec{
+							Privileges: &swarmapi.Privileges{
+								CredentialSpec: &c.from,
+							},
+						},
+					},
+				},
+			}
+			config := containerConfig{task: &task}
+			assert.DeepEqual(t, c.to, config.hostConfig().SecurityOpt)
+		})
+	}
+}

+ 8 - 1
daemon/container_operations_unix.go

@@ -230,7 +230,14 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 	for _, ref := range c.ConfigReferences {
 		// TODO (ehazlett): use type switch when more are supported
 		if ref.File == nil {
-			logrus.Error("config target type is not a file target")
+			// Runtime configs are not mounted into the container, but they're
+			// a valid type of config so we should not error when we encounter
+			// one.
+			if ref.Runtime == nil {
+				logrus.Error("config target type is not a file or runtime target")
+			}
+			// However, in any case, this isn't a file config, so we have no
+			// further work to do
 			continue
 		}
 

+ 8 - 1
daemon/container_operations_windows.go

@@ -44,7 +44,14 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
 	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")
+			// Runtime configs are not mounted into the container, but they're
+			// a valid type of config so we should not error when we encounter
+			// one.
+			if configRef.Runtime == nil {
+				logrus.Error("config target type is not a file or runtime target")
+			}
+			// However, in any case, this isn't a file config, so we have no
+			// further work to do
 			continue
 		}
 

+ 22 - 0
daemon/oci_windows.go

@@ -288,6 +288,28 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
 				if cs, err = readCredentialSpecRegistry(c.ID, csValue); err != nil {
 					return err
 				}
+			} else if match, csValue = getCredentialSpec("config://", splitsOpt[1]); match {
+				// if the container does not have a DependencyStore, then it
+				// isn't swarmkit managed. In order to avoid creating any
+				// impression that `config://` is a valid API, return the same
+				// error as if you'd passed any other random word.
+				if c.DependencyStore == nil {
+					return fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
+				}
+
+				// after this point, we can return regular swarmkit-relevant
+				// errors, because we'll know this container is managed.
+				if csValue == "" {
+					return fmt.Errorf("no value supplied for config:// credential spec security option")
+				}
+
+				csConfig, err := c.DependencyStore.Configs().Get(csValue)
+				if err != nil {
+					return errors.Wrap(err, "error getting value from config store")
+				}
+				// stuff the resulting secret data into a string to use as the
+				// CredentialSpec
+				cs = string(csConfig.Spec.Data)
 			} else {
 				return fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
 			}

+ 4 - 0
docs/api/version-history.md

@@ -29,6 +29,10 @@ keywords: "API, Docker, rcli, REST, documentation"
 * `GET /services/{id}` now returns `Sysctls` as part of the `ContainerSpec`.
 * `POST /services/create` now accepts `Sysctls` as part of the `ContainerSpec`.
 * `POST /services/{id}/update` now accepts `Sysctls` as part of the `ContainerSpec`.
+* `POST /services/create` now accepts `Config` as part of `ContainerSpec.Privileges.CredentialSpec`.
+* `POST /services/{id}/update` now accepts `Config` as part of `ContainerSpec.Privileges.CredentialSpec`.
+* `POST /services/create` now includes `Runtime` as an option in `ContainerSpec.Configs`
+* `POST /services/{id}/update` now includes `Runtime` as an option in `ContainerSpec.Configs`
 * `GET /tasks` now  returns `Sysctls` as part of the `ContainerSpec`.
 * `GET /tasks/{id}` now  returns `Sysctls` as part of the `ContainerSpec`.
 * `GET /nodes` now supports a filter type `node.label` filter to filter nodes based