Ver código fonte

Add support for using Configs as CredentialSpecs in services

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn 6 anos atrás
pai
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
 				// Sysctls for docker swarm services weren't supported before
 				// API version 1.40
 				// API version 1.40
 				service.TaskTemplate.ContainerSpec.Sysctls = nil
 				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 {
 			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
 				// Sysctls for docker swarm services weren't supported before
 				// API version 1.40
 				// API version 1.40
 				service.TaskTemplate.ContainerSpec.Sysctls = nil
 				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 {
 			if service.TaskTemplate.Placement != nil {

+ 13 - 2
api/swagger.yaml

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

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

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

+ 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) {
 func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) {
 	someid := "asfjkl"
 	someid := "asfjkl"
 	s := swarmtypes.ServiceSpec{
 	s := swarmtypes.ServiceSpec{

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

+ 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`.
 * `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`.
 * `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