Browse Source

Merge pull request #23389 from Microsoft/jjh/credentialspec

Windows: Support credential specs
Vincent Demeester 8 years ago
parent
commit
b3cc3d7bf9

+ 6 - 0
api/server/router/build/build_routes.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"runtime"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -51,6 +52,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
 	options.CPUSetMems = r.FormValue("cpusetmems")
 	options.CPUSetMems = r.FormValue("cpusetmems")
 	options.CgroupParent = r.FormValue("cgroupparent")
 	options.CgroupParent = r.FormValue("cgroupparent")
 	options.Tags = r.Form["t"]
 	options.Tags = r.Form["t"]
+	options.SecurityOpt = r.Form["securityopt"]
 
 
 	if r.Form.Get("shmsize") != "" {
 	if r.Form.Get("shmsize") != "" {
 		shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64)
 		shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64)
@@ -67,6 +69,10 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
 		options.Isolation = i
 		options.Isolation = i
 	}
 	}
 
 
+	if runtime.GOOS != "windows" && options.SecurityOpt != nil {
+		return nil, fmt.Errorf("the daemon on this platform does not support --security-opt to build")
+	}
+
 	var buildUlimits = []*units.Ulimit{}
 	var buildUlimits = []*units.Ulimit{}
 	ulimitsJSON := r.FormValue("ulimits")
 	ulimitsJSON := r.FormValue("ulimits")
 	if ulimitsJSON != "" {
 	if ulimitsJSON != "" {

+ 2 - 1
api/types/client.go

@@ -153,7 +153,8 @@ type ImageBuildOptions struct {
 	Squash bool
 	Squash bool
 	// CacheFrom specifies images that are used for matching cache. Images
 	// CacheFrom specifies images that are used for matching cache. Images
 	// specified here do not need to have a valid parent chain to match cache.
 	// specified here do not need to have a valid parent chain to match cache.
-	CacheFrom []string
+	CacheFrom   []string
+	SecurityOpt []string
 }
 }
 
 
 // ImageBuildResponse holds information
 // ImageBuildResponse holds information

+ 4 - 3
builder/dockerfile/internals.go

@@ -484,9 +484,10 @@ func (b *Builder) create() (string, error) {
 
 
 	// TODO: why not embed a hostconfig in builder?
 	// TODO: why not embed a hostconfig in builder?
 	hostConfig := &container.HostConfig{
 	hostConfig := &container.HostConfig{
-		Isolation: b.options.Isolation,
-		ShmSize:   b.options.ShmSize,
-		Resources: resources,
+		SecurityOpt: b.options.SecurityOpt,
+		Isolation:   b.options.Isolation,
+		ShmSize:     b.options.ShmSize,
+		Resources:   resources,
 	}
 	}
 
 
 	config := *b.runConfig
 	config := *b.runConfig

+ 3 - 0
cli/command/image/build.go

@@ -57,6 +57,7 @@ type buildOptions struct {
 	pull           bool
 	pull           bool
 	cacheFrom      []string
 	cacheFrom      []string
 	compress       bool
 	compress       bool
+	securityOpt    []string
 }
 }
 
 
 // NewBuildCommand creates a new `docker build` command
 // NewBuildCommand creates a new `docker build` command
@@ -103,6 +104,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
 	flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
 	flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
 	flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
 	flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
 	flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
+	flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
 
 
 	command.AddTrustedFlags(flags, true)
 	command.AddTrustedFlags(flags, true)
 
 
@@ -299,6 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
 		AuthConfigs:    authConfig,
 		AuthConfigs:    authConfig,
 		Labels:         runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
 		Labels:         runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
 		CacheFrom:      options.cacheFrom,
 		CacheFrom:      options.cacheFrom,
+		SecurityOpt:    options.securityOpt,
 	}
 	}
 
 
 	response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
 	response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)

+ 2 - 1
client/image_build.go

@@ -49,7 +49,8 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
 
 
 func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
 func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
 	query := url.Values{
 	query := url.Values{
-		"t": options.Tags,
+		"t":           options.Tags,
+		"securityopt": options.SecurityOpt,
 	}
 	}
 	if options.SuppressOutput {
 	if options.SuppressOutput {
 		query.Set("q", "1")
 		query.Set("q", "1")

+ 6 - 0
daemon/daemon.go

@@ -547,6 +547,12 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	if runtime.GOOS == "windows" {
+		if err := idtools.MkdirAllAs(filepath.Join(config.Root, "credentialspecs"), 0700, rootUID, rootGID); err != nil && !os.IsExist(err) {
+			return nil, err
+		}
+	}
+
 	driverName := os.Getenv("DOCKER_DRIVER")
 	driverName := os.Getenv("DOCKER_DRIVER")
 	if driverName == "" {
 	if driverName == "" {
 		driverName = config.GraphDriver
 		driverName = config.GraphDriver

+ 101 - 1
daemon/start_windows.go

@@ -2,11 +2,19 @@ package daemon
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"io/ioutil"
 	"path/filepath"
 	"path/filepath"
+	"strings"
 
 
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/libcontainerd"
 	"github.com/docker/docker/libcontainerd"
+	"golang.org/x/sys/windows/registry"
+)
+
+const (
+	credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
+	credentialSpecFileLocation     = "CredentialSpecs"
 )
 )
 
 
 func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) (*[]libcontainerd.CreateOption, error) {
 func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) (*[]libcontainerd.CreateOption, error) {
@@ -80,7 +88,50 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain
 		}
 		}
 	}
 	}
 
 
-	// Now build the full set of options
+	// Read and add credentials from the security options if a credential spec has been provided.
+	if container.HostConfig.SecurityOpt != nil {
+		for _, sOpt := range container.HostConfig.SecurityOpt {
+			sOpt = strings.ToLower(sOpt)
+			if !strings.Contains(sOpt, "=") {
+				return nil, fmt.Errorf("invalid security option: no equals sign in supplied value %s", sOpt)
+			}
+			var splitsOpt []string
+			splitsOpt = strings.SplitN(sOpt, "=", 2)
+			if len(splitsOpt) != 2 {
+				return nil, fmt.Errorf("invalid security option: %s", sOpt)
+			}
+			if splitsOpt[0] != "credentialspec" {
+				return nil, fmt.Errorf("security option not supported: %s", splitsOpt[0])
+			}
+
+			credentialsOpts := &libcontainerd.CredentialsOption{}
+			var (
+				match   bool
+				csValue string
+				err     error
+			)
+			if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match {
+				if csValue == "" {
+					return nil, fmt.Errorf("no value supplied for file:// credential spec security option")
+				}
+				if credentialsOpts.Credentials, err = readCredentialSpecFile(container.ID, daemon.root, filepath.Clean(csValue)); err != nil {
+					return nil, err
+				}
+			} else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match {
+				if csValue == "" {
+					return nil, fmt.Errorf("no value supplied for registry:// credential spec security option")
+				}
+				if credentialsOpts.Credentials, err = readCredentialSpecRegistry(container.ID, csValue); err != nil {
+					return nil, err
+				}
+			} else {
+				return nil, fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
+			}
+			createOptions = append(createOptions, credentialsOpts)
+		}
+	}
+
+	// Now add the remaining options.
 	createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore})
 	createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore})
 	createOptions = append(createOptions, hvOpts)
 	createOptions = append(createOptions, hvOpts)
 	createOptions = append(createOptions, layerOpts)
 	createOptions = append(createOptions, layerOpts)
@@ -90,3 +141,52 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain
 
 
 	return &createOptions, nil
 	return &createOptions, nil
 }
 }
+
+// getCredentialSpec is a helper function to get the value of a credential spec supplied
+// on the CLI, stripping the prefix
+func getCredentialSpec(prefix, value string) (bool, string) {
+	if strings.HasPrefix(value, prefix) {
+		return true, strings.TrimPrefix(value, prefix)
+	}
+	return false, ""
+}
+
+// readCredentialSpecRegistry is a helper function to read a credential spec from
+// the registry. If not found, we return an empty string and warn in the log.
+// This allows for staging on machines which do not have the necessary components.
+func readCredentialSpecRegistry(id, name string) (string, error) {
+	var (
+		k   registry.Key
+		err error
+		val string
+	)
+	if k, err = registry.OpenKey(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE); err != nil {
+		return "", fmt.Errorf("failed handling spec %q for container %s - %s could not be opened", name, id, credentialSpecRegistryLocation)
+	}
+	if val, _, err = k.GetStringValue(name); err != nil {
+		if err == registry.ErrNotExist {
+			return "", fmt.Errorf("credential spec %q for container %s as it was not found", name, id)
+		}
+		return "", fmt.Errorf("error %v reading credential spec %q from registry for container %s", err, name, id)
+	}
+	return val, nil
+}
+
+// readCredentialSpecFile is a helper function to read a credential spec from
+// a file. If not found, we return an empty string and warn in the log.
+// This allows for staging on machines which do not have the necessary components.
+func readCredentialSpecFile(id, root, location string) (string, error) {
+	if filepath.IsAbs(location) {
+		return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute")
+	}
+	base := filepath.Join(root, credentialSpecFileLocation)
+	full := filepath.Join(base, location)
+	if !strings.HasPrefix(full, base) {
+		return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base)
+	}
+	bcontents, err := ioutil.ReadFile(full)
+	if err != nil {
+		return "", fmt.Errorf("credential spec '%s' for container %s as the file could not be read: %q", full, id, err)
+	}
+	return string(bcontents[:]), nil
+}

+ 7 - 0
docs/reference/commandline/build.md

@@ -37,6 +37,7 @@ Options:
       --pull                    Always attempt to pull a newer version of the image
       --pull                    Always attempt to pull a newer version of the image
   -q, --quiet                   Suppress the build output and print image ID on success
   -q, --quiet                   Suppress the build output and print image ID on success
       --rm                      Remove intermediate containers after a successful build (default true)
       --rm                      Remove intermediate containers after a successful build (default true)
+      --security-opt value      Security Options (default [])
       --shm-size string         Size of /dev/shm, default value is 64MB.
       --shm-size string         Size of /dev/shm, default value is 64MB.
                                 The format is `<number><unit>`. `number` must be greater than `0`.
                                 The format is `<number><unit>`. `number` must be greater than `0`.
                                 Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes),
                                 Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes),
@@ -397,6 +398,12 @@ Dockerfile are echoed during the build process.
 For detailed information on using `ARG` and `ENV` instructions, see the
 For detailed information on using `ARG` and `ENV` instructions, see the
 [Dockerfile reference](../builder.md).
 [Dockerfile reference](../builder.md).
 
 
+### Optional security options (--security-opt)
+
+This flag is only supported on a daemon running on Windows, and only supports 
+the `credentialspec` option. The `credentialspec` must be in the format
+`file://spec.txt` or `registry://keyname`. 
+
 ### Specify isolation technology for container (--isolation)
 ### Specify isolation technology for container (--isolation)
 
 
 This option is useful in situations where you are running Docker containers on
 This option is useful in situations where you are running Docker containers on

+ 5 - 0
docs/reference/commandline/run.md

@@ -614,6 +614,11 @@ The `--stop-signal` flag sets the system call signal that will be sent to the co
 This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9,
 This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9,
 or a signal name in the format SIGNAME, for instance SIGKILL.
 or a signal name in the format SIGNAME, for instance SIGKILL.
 
 
+### Optional security options (--security-opt)
+
+On Windows, this flag can be used to specify the `credentialspec` option. 
+The `credentialspec` must be in the format `file://spec.txt` or `registry://keyname`. 
+
 ### Specify isolation technology for container (--isolation)
 ### Specify isolation technology for container (--isolation)
 
 
 This option is useful in situations where you are running Docker containers on
 This option is useful in situations where you are running Docker containers on

+ 19 - 0
integration-cli/docker_cli_run_test.go

@@ -4519,3 +4519,22 @@ func (s *DockerSuite) TestRunStoppedLoggingDriverNoLeak(c *check.C) {
 	// NGoroutines is not updated right away, so we need to wait before failing
 	// NGoroutines is not updated right away, so we need to wait before failing
 	c.Assert(waitForGoroutines(nroutines), checker.IsNil)
 	c.Assert(waitForGoroutines(nroutines), checker.IsNil)
 }
 }
+
+// Handles error conditions for --credentialspec. Validating E2E success cases
+// requires additional infrastructure (AD for example) on CI servers.
+func (s *DockerSuite) TestRunCredentialSpecFailures(c *check.C) {
+	testRequires(c, DaemonIsWindows)
+	attempts := []struct{ value, expectedError string }{
+		{"rubbish", "invalid credential spec security option - value must be prefixed file:// or registry://"},
+		{"rubbish://", "invalid credential spec security option - value must be prefixed file:// or registry://"},
+		{"file://", "no value supplied for file:// credential spec security option"},
+		{"registry://", "no value supplied for registry:// credential spec security option"},
+		{`file://c:\blah.txt`, "path cannot be absolute"},
+		{`file://doesnotexist.txt`, "The system cannot find the file specified"},
+	}
+	for _, attempt := range attempts {
+		_, _, err := dockerCmdWithError("run", "--security-opt=credentialspec="+attempt.value, "busybox", "true")
+		c.Assert(err, checker.NotNil, check.Commentf("%s expected non-nil err", attempt.value))
+		c.Assert(err.Error(), checker.Contains, attempt.expectedError, check.Commentf("%s expected %s got %s", attempt.value, attempt.expectedError, err))
+	}
+}

+ 4 - 0
libcontainerd/client_windows.go

@@ -154,6 +154,10 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir
 			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
 			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
 			continue
 			continue
 		}
 		}
+		if c, ok := option.(*CredentialsOption); ok {
+			configuration.Credentials = c.Credentials
+			continue
+		}
 	}
 	}
 
 
 	// We must have a layer option with at least one path
 	// We must have a layer option with at least one path

+ 6 - 0
libcontainerd/types_windows.go

@@ -62,6 +62,12 @@ type NetworkEndpointsOption struct {
 	AllowUnqualifiedDNSQuery bool
 	AllowUnqualifiedDNSQuery bool
 }
 }
 
 
+// CredentialsOption is a CreateOption that indicates the credentials from
+// a credential spec to be used to the runtime
+type CredentialsOption struct {
+	Credentials string
+}
+
 // Checkpoint holds the details of a checkpoint (not supported in windows)
 // Checkpoint holds the details of a checkpoint (not supported in windows)
 type Checkpoint struct {
 type Checkpoint struct {
 	Name string
 	Name string

+ 5 - 0
libcontainerd/utils_windows.go

@@ -39,3 +39,8 @@ func (h *LayerOption) Apply(interface{}) error {
 func (s *NetworkEndpointsOption) Apply(interface{}) error {
 func (s *NetworkEndpointsOption) Apply(interface{}) error {
 	return nil
 	return nil
 }
 }
+
+// Apply for the credentials option is a no-op.
+func (s *CredentialsOption) Apply(interface{}) error {
+	return nil
+}

+ 1 - 0
runconfig/config.go

@@ -67,6 +67,7 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon
 	if err := ValidateQoS(hc); err != nil {
 	if err := ValidateQoS(hc); err != nil {
 		return nil, nil, nil, err
 		return nil, nil, nil, err
 	}
 	}
+
 	return w.Config, hc, w.NetworkingConfig, nil
 	return w.Config, hc, w.NetworkingConfig, nil
 }
 }
 
 

+ 2 - 0
runconfig/opts/parse.go

@@ -105,6 +105,7 @@ type ContainerOptions struct {
 	autoRemove        bool
 	autoRemove        bool
 	init              bool
 	init              bool
 	initPath          string
 	initPath          string
+	credentialSpec    string
 
 
 	Image string
 	Image string
 	Args  []string
 	Args  []string
@@ -173,6 +174,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
 	flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container")
 	flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container")
 	flags.Var(&copts.securityOpt, "security-opt", "Security Options")
 	flags.Var(&copts.securityOpt, "security-opt", "Security Options")
 	flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use")
 	flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use")
+	flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)")
 
 
 	// Network and port publishing flag
 	// Network and port publishing flag
 	flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
 	flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")