Merge pull request #23389 from Microsoft/jjh/credentialspec
Windows: Support credential specs
This commit is contained in:
commit
b3cc3d7bf9
15 changed files with 173 additions and 6 deletions
|
@ -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 != "" {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
SecurityOpt: b.options.SecurityOpt,
|
||||||
ShmSize: b.options.ShmSize,
|
Isolation: b.options.Isolation,
|
||||||
Resources: resources,
|
ShmSize: b.options.ShmSize,
|
||||||
|
Resources: resources,
|
||||||
}
|
}
|
||||||
|
|
||||||
config := *b.runConfig
|
config := *b.runConfig
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)")
|
||||||
|
|
Loading…
Reference in a new issue