Add support for using Configs as CredentialSpecs in services

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-02-01 15:33:27 +01:00 committed by Drew Erny
parent 04995fa7c7
commit 20383d504b
7 changed files with 300 additions and 25 deletions

View File

@ -218,6 +218,11 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
// 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 = ""
}
}
if service.TaskTemplate.Placement != nil {
@ -270,6 +275,11 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
// 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 = ""
}
}
if service.TaskTemplate.Placement != nil {

View File

@ -2623,8 +2623,19 @@ 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.
<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 +2645,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 +2657,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"

View File

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

View File

@ -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 {
@ -272,22 +266,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 {
@ -359,6 +342,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)

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) {
someid := "asfjkl"
s := swarmtypes.ServiceSpec{

View File

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

View File

@ -29,6 +29,8 @@ 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`.
* `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