1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #38632 from dperny/gmsa-support

Add support for GMSA CredentialSpecs from Swarmkit configs
This commit is contained in:
Brian Goff 2019-02-21 09:05:58 -08:00 committed by GitHub
commit cbb885b07a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 656 additions and 70 deletions

View file

@ -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") { adjustForAPIVersion(cliVersion, &service)
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
}
}
} }
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") { adjustForAPIVersion(cliVersion, &service)
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
}
}
} }
resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry) resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry)

View file

@ -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
}
}
}

View file

@ -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")
}
}

View file

@ -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"

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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{} containerSpec.Privileges.CredentialSpec = credentialSpecFromGRPC(c.Privileges.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()
}
} }
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 { r := &types.ConfigReference{
// not a file target ConfigID: s.ConfigID,
logrus.Warnf("config target not a file: config=%s", s.ConfigID) ConfigName: s.ConfigName,
continue
} }
refs = append(refs, &types.ConfigReference{ if target := s.GetRuntime(); target != nil {
File: &types.ConfigReferenceFileTarget{ 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, } else {
ConfigName: s.ConfigName, // 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{} cs, err := credentialSpecToGRPC(c.Privileges.CredentialSpec)
if err != nil {
if c.Privileges.CredentialSpec.File != "" && c.Privileges.CredentialSpec.Registry != "" { return nil, errors.Wrap(err, "invalid CredentialSpec")
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")
} }
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)

View file

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

View file

@ -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())
} }
} }

View file

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

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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")
} }

View file

@ -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