From 20383d504b99b88cda9771315868b4a31ec2a36e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 1 Feb 2019 15:33:27 +0100 Subject: [PATCH] Add support for using Configs as CredentialSpecs in services Signed-off-by: Sebastiaan van Stijn --- api/server/router/swarm/cluster_routes.go | 10 ++ api/swagger.yaml | 15 +- api/types/swarm/container.go | 1 + daemon/cluster/convert/container.go | 83 ++++++--- daemon/cluster/convert/service_test.go | 161 ++++++++++++++++++ .../executor/container/container_test.go | 53 ++++++ docs/api/version-history.md | 2 + 7 files changed, 300 insertions(+), 25 deletions(-) diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index 509c7acad5..0966d3a5a4 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/api/server/router/swarm/cluster_routes.go @@ -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 { diff --git a/api/swagger.yaml b/api/swagger.yaml index a1f608fad2..124494b364 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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. + +


+ + + > **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:


- > **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:


- > **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" diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index e12f09837f..48190c1762 100644 --- a/api/types/swarm/container.go +++ b/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 } diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 37f562ad26..52e2d0ec61 100644 --- a/daemon/cluster/convert/container.go +++ b/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 { @@ -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) diff --git a/daemon/cluster/convert/service_test.go b/daemon/cluster/convert/service_test.go index ad5f0d4494..a989f391b9 100644 --- a/daemon/cluster/convert/service_test.go +++ b/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{ diff --git a/daemon/cluster/executor/container/container_test.go b/daemon/cluster/executor/container/container_test.go index 5f967c2f77..6a9a72d39b 100644 --- a/daemon/cluster/executor/container/container_test.go +++ b/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) + }) + } +} diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 87e64753c0..b7bb466f12 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -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