Browse Source

Add support for using Configs as CredentialSpecs in services

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn 6 years ago
parent
commit
20383d504b

+ 10 - 0
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 {

+ 13 - 2
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.
+
+                      <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"

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

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

+ 60 - 23
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)

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

@@ -233,6 +233,167 @@ func TestServiceConvertFromGRPCIsolation(t *testing.T) {
 	}
 }
 
+func TestServiceConvertToGRPCCredentialSpec(t *testing.T) {
+	cases := []struct {
+		name        string
+		from        swarmtypes.CredentialSpec
+		to          swarmapi.Privileges_CredentialSpec
+		expectedErr string
+	}{
+		{
+			name:        "empty credential spec",
+			from:        swarmtypes.CredentialSpec{},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: must either provide "file", "registry", or "config" for credential spec`,
+		},
+		{
+			name: "config and file credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config: "0bt9dmxjvjiqermk6xrop3ekq",
+				File:   "spec.json",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "file" credential specs`,
+		},
+		{
+			name: "config and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "registry" credential specs`,
+		},
+		{
+			name: "file and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				File:     "spec.json",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "file" and "registry" credential specs`,
+		},
+		{
+			name: "config and file and registry credential spec",
+			from: swarmtypes.CredentialSpec{
+				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
+				File:     "spec.json",
+				Registry: "testing",
+			},
+			to:          swarmapi.Privileges_CredentialSpec{},
+			expectedErr: `invalid CredentialSpec: cannot specify both "config", "file", and "registry" credential specs`,
+		},
+		{
+			name: "config credential spec",
+			from: swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			},
+		},
+		{
+			name: "file credential spec",
+			from: swarmtypes.CredentialSpec{File: "foo.json"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
+			},
+		},
+		{
+			name: "registry credential spec",
+			from: swarmtypes.CredentialSpec{Registry: "testing"},
+			to: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
+			},
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			s := swarmtypes.ServiceSpec{
+				TaskTemplate: swarmtypes.TaskSpec{
+					ContainerSpec: &swarmtypes.ContainerSpec{
+						Privileges: &swarmtypes.Privileges{
+							CredentialSpec: &c.from,
+						},
+					},
+				},
+			}
+
+			res, err := ServiceSpecToGRPC(s)
+			if c.expectedErr != "" {
+				assert.Error(t, err, c.expectedErr)
+				return
+			}
+
+			assert.NilError(t, err)
+			v, ok := res.Task.Runtime.(*swarmapi.TaskSpec_Container)
+			if !ok {
+				t.Fatal("expected type swarmapi.TaskSpec_Container")
+			}
+			assert.DeepEqual(t, c.to, *v.Container.Privileges.CredentialSpec)
+		})
+	}
+}
+
+func TestServiceConvertFromGRPCCredentialSpec(t *testing.T) {
+	cases := []struct {
+		name string
+		from swarmapi.Privileges_CredentialSpec
+		to   *swarmtypes.CredentialSpec
+	}{
+		{
+			name: "empty credential spec",
+			from: swarmapi.Privileges_CredentialSpec{},
+			to:   &swarmtypes.CredentialSpec{},
+		},
+		{
+			name: "config credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+			},
+			to: &swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
+		},
+		{
+			name: "file credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
+			},
+			to: &swarmtypes.CredentialSpec{File: "foo.json"},
+		},
+		{
+			name: "registry credential spec",
+			from: swarmapi.Privileges_CredentialSpec{
+				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
+			},
+			to: &swarmtypes.CredentialSpec{Registry: "testing"},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			gs := swarmapi.Service{
+				Spec: swarmapi.ServiceSpec{
+					Task: swarmapi.TaskSpec{
+						Runtime: &swarmapi.TaskSpec_Container{
+							Container: &swarmapi.ContainerSpec{
+								Privileges: &swarmapi.Privileges{
+									CredentialSpec: &tc.from,
+								},
+							},
+						},
+					},
+				},
+			}
+
+			svc, err := ServiceFromGRPC(gs)
+			assert.NilError(t, err)
+			assert.DeepEqual(t, svc.Spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec, tc.to)
+		})
+	}
+}
+
 func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) {
 	someid := "asfjkl"
 	s := swarmtypes.ServiceSpec{

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

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

+ 2 - 0
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