Add --device support for Windows

Adds support for --device in Windows. This must take the form of:
--device='class/clsid'. See this post for more information:

https://blogs.technet.microsoft.com/virtualization/2018/08/13/bringing-device-support-to-windows-server-containers/

Signed-off-by: John Howard <jhoward@microsoft.com>
This commit is contained in:
John Howard 2019-01-07 15:34:33 -08:00
parent 896ff57b30
commit 593acf077b
4 changed files with 58 additions and 16 deletions

View File

@ -72,7 +72,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptio
} }
} }
copts.env = *opts.NewListOptsRef(&newEnv, nil) copts.env = *opts.NewListOptsRef(&newEnv, nil)
containerConfig, err := parse(flags, copts) containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
if err != nil { if err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true) reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125} return cli.StatusError{StatusCode: 125}

View File

@ -141,7 +141,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice),
deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
devices: opts.NewListOpts(validateDevice), devices: opts.NewListOpts(nil), // devices can only be validated after we know the server OS
env: opts.NewListOpts(opts.ValidateEnv), env: opts.NewListOpts(opts.ValidateEnv),
envFile: opts.NewListOpts(nil), envFile: opts.NewListOpts(nil),
expose: opts.NewListOpts(nil), expose: opts.NewListOpts(nil),
@ -299,7 +299,7 @@ type containerConfig struct {
// a HostConfig and returns them with the specified command. // a HostConfig and returns them with the specified command.
// If the specified args are not valid, it will return an error. // If the specified args are not valid, it will return an error.
// nolint: gocyclo // nolint: gocyclo
func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, error) { func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
var ( var (
attachStdin = copts.attach.Get("stdin") attachStdin = copts.attach.Get("stdin")
attachStdout = copts.attach.Get("stdout") attachStdout = copts.attach.Get("stdout")
@ -417,10 +417,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
} }
} }
// parse device mappings // validate and parse device mappings. Note we do late validation of the
// device path (as opposed to during flag parsing), as at the time we are
// parsing flags, we haven't yet sent a _ping to the daemon to determine
// what operating system it is.
deviceMappings := []container.DeviceMapping{} deviceMappings := []container.DeviceMapping{}
for _, device := range copts.devices.GetAll() { for _, device := range copts.devices.GetAll() {
deviceMapping, err := parseDevice(device) var (
validated string
deviceMapping container.DeviceMapping
err error
)
validated, err = validateDevice(device, serverOS)
if err != nil {
return nil, err
}
deviceMapping, err = parseDevice(validated, serverOS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -747,7 +759,19 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) {
} }
// parseDevice parses a device mapping string to a container.DeviceMapping struct // parseDevice parses a device mapping string to a container.DeviceMapping struct
func parseDevice(device string) (container.DeviceMapping, error) { func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
switch serverOS {
case "linux":
return parseLinuxDevice(device)
case "windows":
return parseWindowsDevice(device)
}
return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS)
}
// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
// knowing that the target is a Linux daemon
func parseLinuxDevice(device string) (container.DeviceMapping, error) {
src := "" src := ""
dst := "" dst := ""
permissions := "rwm" permissions := "rwm"
@ -781,6 +805,12 @@ func parseDevice(device string) (container.DeviceMapping, error) {
return deviceMapping, nil return deviceMapping, nil
} }
// parseWindowsDevice parses a device mapping string to a container.DeviceMapping struct
// knowing that the target is a Windows daemon
func parseWindowsDevice(device string) (container.DeviceMapping, error) {
return container.DeviceMapping{PathOnHost: device}, nil
}
// validateDeviceCgroupRule validates a device cgroup rule string format // validateDeviceCgroupRule validates a device cgroup rule string format
// It will make sure 'val' is in the form: // It will make sure 'val' is in the form:
// 'type major:minor mode' // 'type major:minor mode'
@ -813,14 +843,23 @@ func validDeviceMode(mode string) bool {
} }
// validateDevice validates a path for devices // validateDevice validates a path for devices
func validateDevice(val string, serverOS string) (string, error) {
switch serverOS {
case "linux":
return validateLinuxPath(val, validDeviceMode)
case "windows":
// Windows does validation entirely server-side
return val, nil
}
return "", errors.Errorf("unknown server OS: %s", serverOS)
}
// validateLinuxPath is the implementation of validateDevice knowing that the
// target server operating system is a Linux daemon.
// It will make sure 'val' is in the form: // It will make sure 'val' is in the form:
// [host-dir:]container-path[:mode] // [host-dir:]container-path[:mode]
// It also validates the device mode. // It also validates the device mode.
func validateDevice(val string) (string, error) { func validateLinuxPath(val string, validator func(string) bool) (string, error) {
return validatePath(val, validDeviceMode)
}
func validatePath(val string, validator func(string) bool) (string, error) {
var containerPath string var containerPath string
var mode string var mode string

View File

@ -16,6 +16,7 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
"gotest.tools/skip"
) )
func TestValidateAttach(t *testing.T) { func TestValidateAttach(t *testing.T) {
@ -48,7 +49,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
return nil, nil, nil, err return nil, nil, nil, err
} }
// TODO: fix tests to accept ContainerConfig // TODO: fix tests to accept ContainerConfig
containerConfig, err := parse(flags, copts) containerConfig, err := parse(flags, copts, runtime.GOOS)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@ -351,6 +352,7 @@ func TestParseWithExpose(t *testing.T) {
} }
func TestParseDevice(t *testing.T) { func TestParseDevice(t *testing.T) {
skip.If(t, runtime.GOOS == "windows") // Windows validates server-side
valids := map[string]container.DeviceMapping{ valids := map[string]container.DeviceMapping{
"/dev/snd": { "/dev/snd": {
PathOnHost: "/dev/snd", PathOnHost: "/dev/snd",
@ -393,7 +395,7 @@ func TestParseModes(t *testing.T) {
flags, copts := setupRunFlags() flags, copts := setupRunFlags()
args := []string{"--pid=container:", "img", "cmd"} args := []string{"--pid=container:", "img", "cmd"}
assert.NilError(t, flags.Parse(args)) assert.NilError(t, flags.Parse(args))
_, err := parse(flags, copts) _, err := parse(flags, copts, runtime.GOOS)
assert.ErrorContains(t, err, "--pid: invalid PID mode") assert.ErrorContains(t, err, "--pid: invalid PID mode")
// pid ok // pid ok
@ -615,6 +617,7 @@ func TestParseEntryPoint(t *testing.T) {
} }
func TestValidateDevice(t *testing.T) { func TestValidateDevice(t *testing.T) {
skip.If(t, runtime.GOOS == "windows") // Windows validates server-side
valid := []string{ valid := []string{
"/home", "/home",
"/home:/home", "/home:/home",
@ -649,13 +652,13 @@ func TestValidateDevice(t *testing.T) {
} }
for _, path := range valid { for _, path := range valid {
if _, err := validateDevice(path); err != nil { if _, err := validateDevice(path, runtime.GOOS); err != nil {
t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err)
} }
} }
for path, expectedError := range invalid { for path, expectedError := range invalid {
if _, err := validateDevice(path); err == nil { if _, err := validateDevice(path, runtime.GOOS); err == nil {
t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
} else { } else {
if err.Error() != expectedError { if err.Error() != expectedError {

View File

@ -78,7 +78,7 @@ func runRun(dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copt
} }
} }
copts.env = *opts.NewListOptsRef(&newEnv, nil) copts.env = *opts.NewListOptsRef(&newEnv, nil)
containerConfig, err := parse(flags, copts) containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
// just in case the parse does not exit // just in case the parse does not exit
if err != nil { if err != nil {
reportError(dockerCli.Err(), "run", err.Error(), true) reportError(dockerCli.Err(), "run", err.Error(), true)