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") {
 		if versions.LessThan(cliVersion, "1.30") {
 			queryRegistry = true
 			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)
 	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") {
 		if versions.LessThan(cliVersion, "1.30") {
 			queryRegistry = true
 			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)
 	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"
 	"github.com/docker/docker/api/server/httputils"
 	basictypes "github.com/docker/docker/api/types"
 	basictypes "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
 	"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
 // 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)
 	httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty)
 	return nil
 	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"
                 type: "object"
                 description: "CredentialSpec for managed service account (Windows only)"
                 description: "CredentialSpec for managed service account (Windows only)"
                 properties:
                 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:
                   File:
                     type: "string"
                     type: "string"
+                    example: "spec.json"
                     description: |
                     description: |
                       Load credential spec from this file. The file is read by the daemon, and must be present in the
                       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
                       `CredentialSpecs` subdirectory in the docker data directory, which defaults to
@@ -2634,7 +2646,7 @@ definitions:
 
 
                       <p><br /></p>
                       <p><br /></p>
 
 
-                      > **Note**: `CredentialSpec.File` and `CredentialSpec.Registry` are mutually exclusive.
+                      > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, and `CredentialSpec.Config` are mutually exclusive.
                   Registry:
                   Registry:
                     type: "string"
                     type: "string"
                     description: |
                     description: |
@@ -2646,7 +2658,7 @@ definitions:
                       <p><br /></p>
                       <p><br /></p>
 
 
 
 
-                      > **Note**: `CredentialSpec.File` and `CredentialSpec.Registry` are mutually exclusive.
+                      > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, and `CredentialSpec.Config` are mutually exclusive.
               SELinuxContext:
               SELinuxContext:
                 type: "object"
                 type: "object"
                 description: "SELinux labels of the container"
                 description: "SELinux labels of the container"
@@ -2757,7 +2769,12 @@ definitions:
               type: "object"
               type: "object"
               properties:
               properties:
                 File:
                 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"
                   type: "object"
                   properties:
                   properties:
                     Name:
                     Name:
@@ -2773,6 +2790,14 @@ definitions:
                       description: "Mode represents the FileMode of the file."
                       description: "Mode represents the FileMode of the file."
                       type: "integer"
                       type: "integer"
                       format: "uint32"
                       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:
                 ConfigID:
                   description: "ConfigID represents the ID of the specific config that we're referencing."
                   description: "ConfigID represents the ID of the specific config that we're referencing."
                   type: "string"
                   type: "string"

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

@@ -27,9 +27,14 @@ type ConfigReferenceFileTarget struct {
 	Mode os.FileMode
 	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
 // ConfigReference is a reference to a config in swarm
 type ConfigReference struct {
 type ConfigReference struct {
-	File       *ConfigReferenceFileTarget
+	File       *ConfigReferenceFileTarget    `json:",omitempty"`
+	Runtime    *ConfigReferenceRuntimeTarget `json:",omitempty"`
 	ConfigID   string
 	ConfigID   string
 	ConfigName string
 	ConfigName string
 }
 }

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

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

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

@@ -1,7 +1,6 @@
 package convert // import "github.com/docker/docker/daemon/cluster/convert"
 package convert // import "github.com/docker/docker/daemon/cluster/convert"
 
 
 import (
 import (
-	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 
 
@@ -10,6 +9,7 @@ import (
 	types "github.com/docker/docker/api/types/swarm"
 	types "github.com/docker/docker/api/types/swarm"
 	swarmapi "github.com/docker/swarmkit/api"
 	swarmapi "github.com/docker/swarmkit/api"
 	gogotypes "github.com/gogo/protobuf/types"
 	gogotypes "github.com/gogo/protobuf/types"
+	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/sirupsen/logrus"
 )
 )
 
 
@@ -52,13 +52,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
 		containerSpec.Privileges = &types.Privileges{}
 		containerSpec.Privileges = &types.Privileges{}
 
 
 		if c.Privileges.CredentialSpec != nil {
 		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 {
 		if c.Privileges.SELinuxContext != nil {
@@ -184,14 +178,26 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef
 	return refs
 	return refs
 }
 }
 
 
-func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigReference {
+func configReferencesToGRPC(sr []*types.ConfigReference) ([]*swarmapi.ConfigReference, error) {
 	refs := make([]*swarmapi.ConfigReference, 0, len(sr))
 	refs := make([]*swarmapi.ConfigReference, 0, len(sr))
 	for _, s := range sr {
 	for _, s := range sr {
 		ref := &swarmapi.ConfigReference{
 		ref := &swarmapi.ConfigReference{
 			ConfigID:   s.ConfigID,
 			ConfigID:   s.ConfigID,
 			ConfigName: s.ConfigName,
 			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{
 			ref.Target = &swarmapi.ConfigReference_File{
 				File: &swarmapi.FileTarget{
 				File: &swarmapi.FileTarget{
 					Name: s.File.Name,
 					Name: s.File.Name,
@@ -205,28 +211,32 @@ func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigRefer
 		refs = append(refs, ref)
 		refs = append(refs, ref)
 	}
 	}
 
 
-	return refs
+	return refs, nil
 }
 }
 
 
 func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigReference {
 func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigReference {
 	refs := make([]*types.ConfigReference, 0, len(sr))
 	refs := make([]*types.ConfigReference, 0, len(sr))
 	for _, s := range 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,
 				Name: target.Name,
 				UID:  target.UID,
 				UID:  target.UID,
 				GID:  target.GID,
 				GID:  target.GID,
 				Mode: target.Mode,
 				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
 	return refs
@@ -249,7 +259,6 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		ReadOnly:   c.ReadOnly,
 		ReadOnly:   c.ReadOnly,
 		Hosts:      c.Hosts,
 		Hosts:      c.Hosts,
 		Secrets:    secretReferencesToGRPC(c.Secrets),
 		Secrets:    secretReferencesToGRPC(c.Secrets),
-		Configs:    configReferencesToGRPC(c.Configs),
 		Isolation:  isolationToGRPC(c.Isolation),
 		Isolation:  isolationToGRPC(c.Isolation),
 		Init:       initToGRPC(c.Init),
 		Init:       initToGRPC(c.Init),
 		Sysctls:    c.Sysctls,
 		Sysctls:    c.Sysctls,
@@ -272,22 +281,11 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		containerSpec.Privileges = &swarmapi.Privileges{}
 		containerSpec.Privileges = &swarmapi.Privileges{}
 
 
 		if c.Privileges.CredentialSpec != nil {
 		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 {
 		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
 	// Mounts
 	for _, m := range c.Mounts {
 	for _, m := range c.Mounts {
 		mount := swarmapi.Mount{
 		mount := swarmapi.Mount{
@@ -359,6 +365,60 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 	return containerSpec, nil
 	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 {
 func healthConfigFromGRPC(h *swarmapi.HealthConfig) *container.HealthConfig {
 	interval, _ := gogotypes.DurationFromProto(h.Interval)
 	interval, _ := gogotypes.DurationFromProto(h.Interval)
 	timeout, _ := gogotypes.DurationFromProto(h.Timeout)
 	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) {
 func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) {
 	someid := "asfjkl"
 	someid := "asfjkl"
 	s := swarmtypes.ServiceSpec{
 	s := swarmtypes.ServiceSpec{
@@ -306,3 +467,147 @@ func TestTaskConvertFromGRPCNetworkAttachment(t *testing.T) {
 		t.Fatalf("expected Runtime to be %v", swarmtypes.RuntimeNetworkAttachment)
 		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())
 			hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=file://"+credentials.GetFile())
 		case *api.Privileges_CredentialSpec_Registry:
 		case *api.Privileges_CredentialSpec_Registry:
 			hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=registry://"+credentials.GetRegistry())
 			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()
 	labels := c.labels()
 	assert.DeepEqual(t, expected, 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 {
 	for _, ref := range c.ConfigReferences {
 		// TODO (ehazlett): use type switch when more are supported
 		// TODO (ehazlett): use type switch when more are supported
 		if ref.File == nil {
 		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
 			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 {
 	for _, configRef := range c.ConfigReferences {
 		// TODO (ehazlett): use type switch when more are supported
 		// TODO (ehazlett): use type switch when more are supported
 		if configRef.File == nil {
 		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
 			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 {
 				if cs, err = readCredentialSpecRegistry(c.ID, csValue); err != nil {
 					return err
 					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 {
 			} else {
 				return fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
 				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`.
 * `GET /services/{id}` now returns `Sysctls` as part of the `ContainerSpec`.
 * `POST /services/create` now accepts `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/{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` now  returns `Sysctls` as part of the `ContainerSpec`.
 * `GET /tasks/{id}` 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
 * `GET /nodes` now supports a filter type `node.label` filter to filter nodes based