Introduce `://` syntax for Windows Devices in DeviceMapping.PathOnHost
**- What I did**

Introduced a new Windows Device string syntax for the `--devices` and the `api.types.container.DeviceMapping.PathOnHost`.

The syntax is `IDType://ID`, reflecting an evolution and generalisation of the existing `class/ID` syntax. The latter remains supported specifically for `class/X`: `IDType/ID` for other `IDType`s is not implemented to avoid future parsing ambiguity.

This syntax will function with both the `--device` parameter from https://github.com/docker/cli, and also when fed from Kubernetes (or other CRI client) via https://github.com/Mirantis/cri-dockerd, or [dockershim](https://kubernetes.io/blog/2022/02/17/dockershim-faq/) where supported. Kubernetes and CRI are both currently agnostic about this format, passing the strings down from user-controlled APIs unchanged.

~~`DeviceMapping` no long accepts-and-ignores values in the `PathInContainer` or `CgroupPermissions` fields for Windows Devices.~~ (This will be a separate to-be-created PR, as this was a more-impactful change than I realised)

For reference, containerd has just [implemented the same syntax](https://github.com/containerd/containerd/pull/6618) for both CRI and the `ctr` CLI, to be part of the 1.7 release series. When [a more-specific CRI type is implemented for Windows Devices](https://github.com/kubernetes/kubernetes/issues/97739), then cri-dockerd (or other CRI implementation) will be responsible for packing the required data into this format for consumption by the daemon.

Windows Devices are no-longer limited to Process Isolation mode, as there are IDTypes that are also supported for HyperV Isolation, and I don't think it's desirable to repeat the mapping of which IDTypes work with which Isolation mode here.

**Note**: Due to limitations of the HCS v1 API used when not using the containerd backend:
* IDTypes other than `class` or `vpci-class-guid` are not supported without the containerd backend.
* Assigning devices to HyperV-isolated containers is not supported without the containerd backend.

Because these depend on the backend in-use, they are reported as failures from the backend via `ContainerStart` when not supported. Previously, `ContainerCreate` rejected both of these cases unconditionally, so these restrictions are looser than they were before this change.

Practically, these limitations from using the HCS v1 API won't matter, since Windows Server 2019 and earlier do not support the other IDType mappings, even if using the HCS v2 API via containerd, and Windows Server 2022 and later is intended to default to using the containerd backend and the HCS v2 API in Docker 21.xx. The only other Windows Server product not already end-of-life is SAC 20H2, [which does support the newer ID types](https://github.com/kubernetes/kubernetes/issues/97739#issuecomment-767981773). That product, the final Windows Server SAC release, reaches end-of-life in August 2022.

**- How I did it**

Changed the code that split `DeviceMapping{PathOnHost: "class/X"}` into `WindowsDevice{IDType:"class", ID: "X"}` to also split `DeviceMapping{PathOnHost: "W://X"}` into `WindowsDevice{IDType:"W", ID: "X"}`

**- How to verify it**

Unit test coverage for the parser.

Integration tests covering `class/5B45201D-F2F2-4F3B-85BB-30FF1F953599`, `class://5B45201D-F2F2-4F3B-85BB-30FF1F953599` and `vpci-class-guid://5B45201D-F2F2-4F3B-85BB-30FF1F953599` for both Process Isolation and HyperV isolation, supported by both containerd and HCS v1-based backends, where possible.

*Note*: Integration tests were having issues with HyperV isolation, so they have been skipped except for the early-failure case. It turns out we currently have no integration tests for HyperV isolation running on CI. This is possibly a nested-VM issue on the CI side, so it may vary by build-host config. I saw one pass for the skipped tests on an RS5 node.

Manually test exactly like the existing `--device` CLI flag on Windows, except using `class://` where you would currently use `class/`, e.g. `class/5B45201D-F2F2-4F3B-85BB-30FF1F953599` and `class://5B45201D-F2F2-4F3B-85BB-30FF1F953599` should produce identical results.

hcshim defines [a bunch of other possible IDTypes](https://github.com/microsoft/hcsshim/blob/v0.9.2/internal/uvm/virtual_device.go#L14-L21), but platform support and use-cases will vary, so I can't really advise on how to test them. And we don't deal with them here anyway, they go on opaquely down to hcshim to be handled.

**- Description for the changelog**

Windows Devices described by string, e.g. `--devices` in the CLI, now support the syntax `IDType://ID` as well as `class/ID`. Supported IDTypes depend on the active backend and other parameters of the container being created.

**- A picture of a cute animal (not mandatory but encouraged)**

[![Baby Animal](https://static.wikia.nocookie.net/muppet/images/d/dc/Character.babyanimal.jpg)](https://muppet.fandom.com/wiki/Muppet_Babies_(puppets)?file=Character.babyanimal.jpg)
daemon/oci_windows.go
package daemon // import "github.com/docker/docker/daemon"

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/oci"
	"github.com/docker/docker/pkg/sysinfo"
	"github.com/docker/docker/pkg/system"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/windows/registry"
)

const (
	credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
	credentialSpecFileLocation     = "CredentialSpecs"
)

func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {

	img, err := daemon.imageService.GetImage(string(c.ImageID), nil)
	if err != nil {
		return nil, err
	}
	if !system.IsOSSupported(img.OperatingSystem()) {
		return nil, system.ErrNotSupportedOperatingSystem
	}

	s := oci.DefaultSpec()

	linkedEnv, err := daemon.setupLinkedContainers(c)
	if err != nil {
		return nil, err
	}

	// Note, unlike Unix, we do NOT call into SetupWorkingDirectory as
	// this is done in VMCompute. Further, we couldn't do it for Hyper-V
	// containers anyway.

	if err := daemon.setupSecretDir(c); err != nil {
		return nil, err
	}

	if err := daemon.setupConfigDir(c); err != nil {
		return nil, err
	}

	// In s.Mounts
	mounts, err := daemon.setupMounts(c)
	if err != nil {
		return nil, err
	}

	var isHyperV bool
	if c.HostConfig.Isolation.IsDefault() {
		// Container using default isolation, so take the default from the daemon configuration
		isHyperV = daemon.defaultIsolation.IsHyperV()
	} else {
		// Container may be requesting an explicit isolation mode.
		isHyperV = c.HostConfig.Isolation.IsHyperV()
	}

	if isHyperV {
		s.Windows.HyperV = &specs.WindowsHyperV{}
	}

	// If the container has not been started, and has configs or secrets
	// secrets, create symlinks to each config and secret. If it has been
	// started before, the symlinks should have already been created. Also, it
	// is important to not mount a Hyper-V  container that has been started
	// before, to protect the host from the container; for example, from
	// malicious mutation of NTFS data structures.
	if !c.HasBeenStartedBefore && (len(c.SecretReferences) > 0 || len(c.ConfigReferences) > 0) {
		// The container file system is mounted before this function is called,
		// except for Hyper-V containers, so mount it here in that case.
		if isHyperV {
			if err := daemon.Mount(c); err != nil {
				return nil, err
			}
			defer daemon.Unmount(c)
		}
		if err := c.CreateSecretSymlinks(); err != nil {
			return nil, err
		}
		if err := c.CreateConfigSymlinks(); err != nil {
			return nil, err
		}
	}

	secretMounts, err := c.SecretMounts()
	if err != nil {
		return nil, err
	}
	if secretMounts != nil {
		mounts = append(mounts, secretMounts...)
	}

	configMounts := c.ConfigMounts()
	if configMounts != nil {
		mounts = append(mounts, configMounts...)
	}

	for _, mount := range mounts {
		m := specs.Mount{
			Source:      mount.Source,
			Destination: mount.Destination,
		}
		if !mount.Writable {
			m.Options = append(m.Options, "ro")
		}
		s.Mounts = append(s.Mounts, m)
	}

	// In s.Process
	s.Process.Cwd = c.Config.WorkingDir
	s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv)
	s.Process.Terminal = c.Config.Tty

	if c.Config.Tty {
		s.Process.ConsoleSize = &specs.Box{
			Height: c.HostConfig.ConsoleSize[0],
			Width:  c.HostConfig.ConsoleSize[1],
		}
	}
	s.Process.User.Username = c.Config.User
	s.Windows.LayerFolders, err = daemon.imageService.GetLayerFolders(img, c.RWLayer)
	if err != nil {
		return nil, errors.Wrapf(err, "container %s", c.ID)
	}

	dnsSearch := daemon.getDNSSearchSettings(c)

	// Get endpoints for the libnetwork allocated networks to the container
	var epList []string
	AllowUnqualifiedDNSQuery := false
	gwHNSID := ""
	if c.NetworkSettings != nil {
		for n := range c.NetworkSettings.Networks {
			sn, err := daemon.FindNetwork(n)
			if err != nil {
				continue
			}

			ep, err := getEndpointInNetwork(c.Name, sn)
			if err != nil {
				continue
			}

			data, err := ep.DriverInfo()
			if err != nil {
				continue
			}

			if data["GW_INFO"] != nil {
				gwInfo := data["GW_INFO"].(map[string]interface{})
				if gwInfo["hnsid"] != nil {
					gwHNSID = gwInfo["hnsid"].(string)
				}
			}

			if data["hnsid"] != nil {
				epList = append(epList, data["hnsid"].(string))
			}

			if data["AllowUnqualifiedDNSQuery"] != nil {
				AllowUnqualifiedDNSQuery = true
			}
		}
	}

	var networkSharedContainerID string
	if c.HostConfig.NetworkMode.IsContainer() {
		networkSharedContainerID = c.NetworkSharedContainerID
		for _, ep := range c.SharedEndpointList {
			epList = append(epList, ep)
		}
	}

	if gwHNSID != "" {
		epList = append(epList, gwHNSID)
	}

	s.Windows.Network = &specs.WindowsNetwork{
		AllowUnqualifiedDNSQuery:   AllowUnqualifiedDNSQuery,
		DNSSearchList:              dnsSearch,
		EndpointList:               epList,
		NetworkSharedContainerName: networkSharedContainerID,
	}

	if err := daemon.createSpecWindowsFields(c, &s, isHyperV); err != nil {
		return nil, err
	}

	if logrus.IsLevelEnabled(logrus.DebugLevel) {
		if b, err := json.Marshal(&s); err == nil {
			logrus.Debugf("Generated spec: %s", string(b))
		}
	}

	return &s, nil
}

// Sets the Windows-specific fields of the OCI spec
func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) error {

	s.Hostname = c.FullHostname()

	if len(s.Process.Cwd) == 0 {
		// We default to C:\ to workaround the oddity of the case that the
		// default directory for cmd running as LocalSystem (or
		// ContainerAdministrator) is c:\windows\system32. Hence docker run
		// <image> cmd will by default end in c:\windows\system32, rather
		// than 'root' (/) on Linux. The oddity is that if you have a dockerfile
		// which has no WORKDIR and has a COPY file ., . will be interpreted
		// as c:\. Hence, setting it to default of c:\ makes for consistency.
		s.Process.Cwd = `C:\`
	}

	if c.Config.ArgsEscaped {
		s.Process.CommandLine = c.Path
		if len(c.Args) > 0 {
			s.Process.CommandLine += " " + system.EscapeArgs(c.Args)
		}
	} else {
		s.Process.Args = append([]string{c.Path}, c.Args...)
	}
	s.Root.Readonly = false // Windows does not support a read-only root filesystem
	if !isHyperV {
		if c.BaseFS == nil {
			return errors.New("createSpecWindowsFields: BaseFS of container " + c.ID + " is unexpectedly nil")
		}

		s.Root.Path = c.BaseFS.Path() // This is not set for Hyper-V containers
		if !strings.HasSuffix(s.Root.Path, `\`) {
			s.Root.Path = s.Root.Path + `\` // Ensure a correctly formatted volume GUID path \\?\Volume{GUID}\
		}
	}

	// First boot optimization
	s.Windows.IgnoreFlushesDuringBoot = !c.HasBeenStartedBefore

	setResourcesInSpec(c, s, isHyperV)

	// Read and add credentials from the security options if a credential spec has been provided.
	if err := daemon.setWindowsCredentialSpec(c, s); err != nil {
		return err
	}

	// Do we have any assigned devices?
	if len(c.HostConfig.Devices) > 0 {
		if isHyperV {
			return errors.New("device assignment is not supported for HyperV containers")
		}
		for _, deviceMapping := range c.HostConfig.Devices {
			srcParts := strings.SplitN(deviceMapping.PathOnHost, "/", 2)
			if len(srcParts) != 2 {
				return errors.New("invalid device assignment path")
			}
			if srcParts[0] != "class" {
				return errors.Errorf("invalid device assignment type: '%s' should be 'class'", srcParts[0])
			}
			wd := specs.WindowsDevice{
				ID:     srcParts[1],
				IDType: srcParts[0],
			}
			s.Windows.Devices = append(s.Windows.Devices, wd)
		}
	}

	return nil
}

var errInvalidCredentialSpecSecOpt = errdefs.InvalidParameter(fmt.Errorf("invalid credential spec security option - value must be prefixed by 'file://', 'registry://', or 'raw://' followed by a non-empty value"))

// setWindowsCredentialSpec sets the spec's `Windows.CredentialSpec`
// field if relevant
func (daemon *Daemon) setWindowsCredentialSpec(c *container.Container, s *specs.Spec) error {
	if c.HostConfig == nil || c.HostConfig.SecurityOpt == nil {
		return nil
	}

	// TODO (jrouge/wk8): if provided with several security options, we silently ignore
	// all but the last one (provided they're all valid, otherwise we do return an error);
	// this doesn't seem like a great idea?
	credentialSpec := ""

	for _, secOpt := range c.HostConfig.SecurityOpt {
		optSplits := strings.SplitN(secOpt, "=", 2)
		if len(optSplits) != 2 {
			return errdefs.InvalidParameter(fmt.Errorf("invalid security option: no equals sign in supplied value %s", secOpt))
		}
		if !strings.EqualFold(optSplits[0], "credentialspec") {
			return errdefs.InvalidParameter(fmt.Errorf("security option not supported: %s", optSplits[0]))
		}

		credSpecSplits := strings.SplitN(optSplits[1], "://", 2)
		if len(credSpecSplits) != 2 || credSpecSplits[1] == "" {
			return errInvalidCredentialSpecSecOpt
		}
		value := credSpecSplits[1]

		var err error
		switch strings.ToLower(credSpecSplits[0]) {
		case "file":
			if credentialSpec, err = readCredentialSpecFile(c.ID, daemon.root, filepath.Clean(value)); err != nil {
				return errdefs.InvalidParameter(err)
			}
		case "registry":
			if credentialSpec, err = readCredentialSpecRegistry(c.ID, value); err != nil {
				return errdefs.InvalidParameter(err)
			}
		case "config":
			// if the container does not have a DependencyStore, then it
			// isn't swarmkit managed. In order to avoid creating any
			// impression that `config://` is a valid API, return the same
			// error as if you'd passed any other random word.
			if c.DependencyStore == nil {
				return errInvalidCredentialSpecSecOpt
			}

			csConfig, err := c.DependencyStore.Configs().Get(value)
			if err != nil {
				return errdefs.System(errors.Wrap(err, "error getting value from config store"))
			}
			// stuff the resulting secret data into a string to use as the
			// CredentialSpec
			credentialSpec = string(csConfig.Spec.Data)
		case "raw":
			credentialSpec = value
		default:
			return errInvalidCredentialSpecSecOpt
		}
	}

	if credentialSpec != "" {
		if s.Windows == nil {
			s.Windows = &specs.Windows{}
		}
		s.Windows.CredentialSpec = credentialSpec
	}

	return nil
}

func setResourcesInSpec(c *container.Container, s *specs.Spec, isHyperV bool) {
	// In s.Windows.Resources
	cpuShares := uint16(c.HostConfig.CPUShares)
	cpuMaximum := uint16(c.HostConfig.CPUPercent) * 100
	cpuCount := uint64(c.HostConfig.CPUCount)
	if c.HostConfig.NanoCPUs > 0 {
		if isHyperV {
			cpuCount = uint64(c.HostConfig.NanoCPUs / 1e9)
			leftoverNanoCPUs := c.HostConfig.NanoCPUs % 1e9
			if leftoverNanoCPUs != 0 {
				cpuCount++
				cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(cpuCount) / (1e9 / 10000))
				if cpuMaximum < 1 {
					// The requested NanoCPUs is so small that we rounded to 0, use 1 instead
					cpuMaximum = 1
				}
			}
		} else {
			cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(sysinfo.NumCPU()) / (1e9 / 10000))
			if cpuMaximum < 1 {
				// The requested NanoCPUs is so small that we rounded to 0, use 1 instead
				cpuMaximum = 1
			}
		}
	}

	if cpuMaximum != 0 || cpuShares != 0 || cpuCount != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.CPU = &specs.WindowsCPUResources{
			Maximum: &cpuMaximum,
			Shares:  &cpuShares,
			Count:   &cpuCount,
		}
	}

	memoryLimit := uint64(c.HostConfig.Memory)
	if memoryLimit != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.Memory = &specs.WindowsMemoryResources{
			Limit: &memoryLimit,
		}
	}

	if c.HostConfig.IOMaximumBandwidth != 0 || c.HostConfig.IOMaximumIOps != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.Storage = &specs.WindowsStorageResources{
			Bps:  &c.HostConfig.IOMaximumBandwidth,
			Iops: &c.HostConfig.IOMaximumIOps,
		}
	}
}

// mergeUlimits merge the Ulimits from HostConfig with daemon defaults, and update HostConfig
// It will do nothing on non-Linux platform
func (daemon *Daemon) mergeUlimits(c *containertypes.HostConfig) {
	return
}

// registryKey is an interface wrapper around `registry.Key`,
// listing only the methods we care about here.
// It's mainly useful to easily allow mocking the registry in tests.
type registryKey interface {
	GetStringValue(name string) (val string, valtype uint32, err error)
	Close() error
}

var registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
	return registry.OpenKey(baseKey, path, access)
}

// 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) {
	key, err := registryOpenKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE)
	if err != nil {
		return "", errors.Wrapf(err, "failed handling spec %q for container %s - registry key %s could not be opened", name, id, credentialSpecRegistryLocation)
	}
	defer key.Close()

	value, _, err := key.GetStringValue(name)
	if err != nil {
		if err == registry.ErrNotExist {
			return "", fmt.Errorf("registry credential spec %q for container %s was not found", name, id)
		}
		return "", errors.Wrapf(err, "error reading credential spec %q from registry for container %s", name, id)
	}

	return value, 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 := os.ReadFile(full)
	if err != nil {
		return "", errors.Wrapf(err, "credential spec for container %s could not be read from file %q", id, full)
	}
	return string(bcontents[:]), nil
}

package daemon // import "github.com/docker/docker/daemon"

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/oci"
	"github.com/docker/docker/pkg/sysinfo"
	"github.com/docker/docker/pkg/system"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/windows/registry"
)

const (
	credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
	credentialSpecFileLocation     = "CredentialSpecs"
)

func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {

	img, err := daemon.imageService.GetImage(string(c.ImageID), nil)
	if err != nil {
		return nil, err
	}
	if !system.IsOSSupported(img.OperatingSystem()) {
		return nil, system.ErrNotSupportedOperatingSystem
	}

	s := oci.DefaultSpec()

	linkedEnv, err := daemon.setupLinkedContainers(c)
	if err != nil {
		return nil, err
	}

	// Note, unlike Unix, we do NOT call into SetupWorkingDirectory as
	// this is done in VMCompute. Further, we couldn't do it for Hyper-V
	// containers anyway.

	if err := daemon.setupSecretDir(c); err != nil {
		return nil, err
	}

	if err := daemon.setupConfigDir(c); err != nil {
		return nil, err
	}

	// In s.Mounts
	mounts, err := daemon.setupMounts(c)
	if err != nil {
		return nil, err
	}

	var isHyperV bool
	if c.HostConfig.Isolation.IsDefault() {
		// Container using default isolation, so take the default from the daemon configuration
		isHyperV = daemon.defaultIsolation.IsHyperV()
	} else {
		// Container may be requesting an explicit isolation mode.
		isHyperV = c.HostConfig.Isolation.IsHyperV()
	}

	if isHyperV {
		s.Windows.HyperV = &specs.WindowsHyperV{}
	}

	// If the container has not been started, and has configs or secrets
	// secrets, create symlinks to each config and secret. If it has been
	// started before, the symlinks should have already been created. Also, it
	// is important to not mount a Hyper-V  container that has been started
	// before, to protect the host from the container; for example, from
	// malicious mutation of NTFS data structures.
	if !c.HasBeenStartedBefore && (len(c.SecretReferences) > 0 || len(c.ConfigReferences) > 0) {
		// The container file system is mounted before this function is called,
		// except for Hyper-V containers, so mount it here in that case.
		if isHyperV {
			if err := daemon.Mount(c); err != nil {
				return nil, err
			}
			defer daemon.Unmount(c)
		}
		if err := c.CreateSecretSymlinks(); err != nil {
			return nil, err
		}
		if err := c.CreateConfigSymlinks(); err != nil {
			return nil, err
		}
	}

	secretMounts, err := c.SecretMounts()
	if err != nil {
		return nil, err
	}
	if secretMounts != nil {
		mounts = append(mounts, secretMounts...)
	}

	configMounts := c.ConfigMounts()
	if configMounts != nil {
		mounts = append(mounts, configMounts...)
	}

	for _, mount := range mounts {
		m := specs.Mount{
			Source:      mount.Source,
			Destination: mount.Destination,
		}
		if !mount.Writable {
			m.Options = append(m.Options, "ro")
		}
		s.Mounts = append(s.Mounts, m)
	}

	// In s.Process
	s.Process.Cwd = c.Config.WorkingDir
	s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv)
	s.Process.Terminal = c.Config.Tty

	if c.Config.Tty {
		s.Process.ConsoleSize = &specs.Box{
			Height: c.HostConfig.ConsoleSize[0],
			Width:  c.HostConfig.ConsoleSize[1],
		}
	}
	s.Process.User.Username = c.Config.User
	s.Windows.LayerFolders, err = daemon.imageService.GetLayerFolders(img, c.RWLayer)
	if err != nil {
		return nil, errors.Wrapf(err, "container %s", c.ID)
	}

	dnsSearch := daemon.getDNSSearchSettings(c)

	// Get endpoints for the libnetwork allocated networks to the container
	var epList []string
	AllowUnqualifiedDNSQuery := false
	gwHNSID := ""
	if c.NetworkSettings != nil {
		for n := range c.NetworkSettings.Networks {
			sn, err := daemon.FindNetwork(n)
			if err != nil {
				continue
			}

			ep, err := getEndpointInNetwork(c.Name, sn)
			if err != nil {
				continue
			}

			data, err := ep.DriverInfo()
			if err != nil {
				continue
			}

			if data["GW_INFO"] != nil {
				gwInfo := data["GW_INFO"].(map[string]interface{})
				if gwInfo["hnsid"] != nil {
					gwHNSID = gwInfo["hnsid"].(string)
				}
			}

			if data["hnsid"] != nil {
				epList = append(epList, data["hnsid"].(string))
			}

			if data["AllowUnqualifiedDNSQuery"] != nil {
				AllowUnqualifiedDNSQuery = true
			}
		}
	}

	var networkSharedContainerID string
	if c.HostConfig.NetworkMode.IsContainer() {
		networkSharedContainerID = c.NetworkSharedContainerID
		for _, ep := range c.SharedEndpointList {
			epList = append(epList, ep)
		}
	}

	if gwHNSID != "" {
		epList = append(epList, gwHNSID)
	}

	s.Windows.Network = &specs.WindowsNetwork{
		AllowUnqualifiedDNSQuery:   AllowUnqualifiedDNSQuery,
		DNSSearchList:              dnsSearch,
		EndpointList:               epList,
		NetworkSharedContainerName: networkSharedContainerID,
	}

	if err := daemon.createSpecWindowsFields(c, &s, isHyperV); err != nil {
		return nil, err
	}

	if logrus.IsLevelEnabled(logrus.DebugLevel) {
		if b, err := json.Marshal(&s); err == nil {
			logrus.Debugf("Generated spec: %s", string(b))
		}
	}

	return &s, nil
}

// Sets the Windows-specific fields of the OCI spec
func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) error {

	s.Hostname = c.FullHostname()

	if len(s.Process.Cwd) == 0 {
		// We default to C:\ to workaround the oddity of the case that the
		// default directory for cmd running as LocalSystem (or
		// ContainerAdministrator) is c:\windows\system32. Hence docker run
		// <image> cmd will by default end in c:\windows\system32, rather
		// than 'root' (/) on Linux. The oddity is that if you have a dockerfile
		// which has no WORKDIR and has a COPY file ., . will be interpreted
		// as c:\. Hence, setting it to default of c:\ makes for consistency.
		s.Process.Cwd = `C:\`
	}

	if c.Config.ArgsEscaped {
		s.Process.CommandLine = c.Path
		if len(c.Args) > 0 {
			s.Process.CommandLine += " " + system.EscapeArgs(c.Args)
		}
	} else {
		s.Process.Args = append([]string{c.Path}, c.Args...)
	}
	s.Root.Readonly = false // Windows does not support a read-only root filesystem
	if !isHyperV {
		if c.BaseFS == nil {
			return errors.New("createSpecWindowsFields: BaseFS of container " + c.ID + " is unexpectedly nil")
		}

		s.Root.Path = c.BaseFS.Path() // This is not set for Hyper-V containers
		if !strings.HasSuffix(s.Root.Path, `\`) {
			s.Root.Path = s.Root.Path + `\` // Ensure a correctly formatted volume GUID path \\?\Volume{GUID}\
		}
	}

	// First boot optimization
	s.Windows.IgnoreFlushesDuringBoot = !c.HasBeenStartedBefore

	setResourcesInSpec(c, s, isHyperV)

	// Read and add credentials from the security options if a credential spec has been provided.
	if err := daemon.setWindowsCredentialSpec(c, s); err != nil {
		return err
	}

	devices, err := setupWindowsDevices(c.HostConfig.Devices)
	if err != nil {
		return err
	}

	s.Windows.Devices = append(s.Windows.Devices, devices...)

	return nil
}

var errInvalidCredentialSpecSecOpt = errdefs.InvalidParameter(fmt.Errorf("invalid credential spec security option - value must be prefixed by 'file://', 'registry://', or 'raw://' followed by a non-empty value"))

// setWindowsCredentialSpec sets the spec's `Windows.CredentialSpec`
// field if relevant
func (daemon *Daemon) setWindowsCredentialSpec(c *container.Container, s *specs.Spec) error {
	if c.HostConfig == nil || c.HostConfig.SecurityOpt == nil {
		return nil
	}

	// TODO (jrouge/wk8): if provided with several security options, we silently ignore
	// all but the last one (provided they're all valid, otherwise we do return an error);
	// this doesn't seem like a great idea?
	credentialSpec := ""

	for _, secOpt := range c.HostConfig.SecurityOpt {
		optSplits := strings.SplitN(secOpt, "=", 2)
		if len(optSplits) != 2 {
			return errdefs.InvalidParameter(fmt.Errorf("invalid security option: no equals sign in supplied value %s", secOpt))
		}
		if !strings.EqualFold(optSplits[0], "credentialspec") {
			return errdefs.InvalidParameter(fmt.Errorf("security option not supported: %s", optSplits[0]))
		}

		credSpecSplits := strings.SplitN(optSplits[1], "://", 2)
		if len(credSpecSplits) != 2 || credSpecSplits[1] == "" {
			return errInvalidCredentialSpecSecOpt
		}
		value := credSpecSplits[1]

		var err error
		switch strings.ToLower(credSpecSplits[0]) {
		case "file":
			if credentialSpec, err = readCredentialSpecFile(c.ID, daemon.root, filepath.Clean(value)); err != nil {
				return errdefs.InvalidParameter(err)
			}
		case "registry":
			if credentialSpec, err = readCredentialSpecRegistry(c.ID, value); err != nil {
				return errdefs.InvalidParameter(err)
			}
		case "config":
			// if the container does not have a DependencyStore, then it
			// isn't swarmkit managed. In order to avoid creating any
			// impression that `config://` is a valid API, return the same
			// error as if you'd passed any other random word.
			if c.DependencyStore == nil {
				return errInvalidCredentialSpecSecOpt
			}

			csConfig, err := c.DependencyStore.Configs().Get(value)
			if err != nil {
				return errdefs.System(errors.Wrap(err, "error getting value from config store"))
			}
			// stuff the resulting secret data into a string to use as the
			// CredentialSpec
			credentialSpec = string(csConfig.Spec.Data)
		case "raw":
			credentialSpec = value
		default:
			return errInvalidCredentialSpecSecOpt
		}
	}

	if credentialSpec != "" {
		if s.Windows == nil {
			s.Windows = &specs.Windows{}
		}
		s.Windows.CredentialSpec = credentialSpec
	}

	return nil
}

func setResourcesInSpec(c *container.Container, s *specs.Spec, isHyperV bool) {
	// In s.Windows.Resources
	cpuShares := uint16(c.HostConfig.CPUShares)
	cpuMaximum := uint16(c.HostConfig.CPUPercent) * 100
	cpuCount := uint64(c.HostConfig.CPUCount)
	if c.HostConfig.NanoCPUs > 0 {
		if isHyperV {
			cpuCount = uint64(c.HostConfig.NanoCPUs / 1e9)
			leftoverNanoCPUs := c.HostConfig.NanoCPUs % 1e9
			if leftoverNanoCPUs != 0 {
				cpuCount++
				cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(cpuCount) / (1e9 / 10000))
				if cpuMaximum < 1 {
					// The requested NanoCPUs is so small that we rounded to 0, use 1 instead
					cpuMaximum = 1
				}
			}
		} else {
			cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(sysinfo.NumCPU()) / (1e9 / 10000))
			if cpuMaximum < 1 {
				// The requested NanoCPUs is so small that we rounded to 0, use 1 instead
				cpuMaximum = 1
			}
		}
	}

	if cpuMaximum != 0 || cpuShares != 0 || cpuCount != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.CPU = &specs.WindowsCPUResources{
			Maximum: &cpuMaximum,
			Shares:  &cpuShares,
			Count:   &cpuCount,
		}
	}

	memoryLimit := uint64(c.HostConfig.Memory)
	if memoryLimit != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.Memory = &specs.WindowsMemoryResources{
			Limit: &memoryLimit,
		}
	}

	if c.HostConfig.IOMaximumBandwidth != 0 || c.HostConfig.IOMaximumIOps != 0 {
		if s.Windows.Resources == nil {
			s.Windows.Resources = &specs.WindowsResources{}
		}
		s.Windows.Resources.Storage = &specs.WindowsStorageResources{
			Bps:  &c.HostConfig.IOMaximumBandwidth,
			Iops: &c.HostConfig.IOMaximumIOps,
		}
	}
}

// mergeUlimits merge the Ulimits from HostConfig with daemon defaults, and update HostConfig
// It will do nothing on non-Linux platform
func (daemon *Daemon) mergeUlimits(c *containertypes.HostConfig) {
	return
}

// registryKey is an interface wrapper around `registry.Key`,
// listing only the methods we care about here.
// It's mainly useful to easily allow mocking the registry in tests.
type registryKey interface {
	GetStringValue(name string) (val string, valtype uint32, err error)
	Close() error
}

var registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
	return registry.OpenKey(baseKey, path, access)
}

// 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) {
	key, err := registryOpenKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE)
	if err != nil {
		return "", errors.Wrapf(err, "failed handling spec %q for container %s - registry key %s could not be opened", name, id, credentialSpecRegistryLocation)
	}
	defer key.Close()

	value, _, err := key.GetStringValue(name)
	if err != nil {
		if err == registry.ErrNotExist {
			return "", fmt.Errorf("registry credential spec %q for container %s was not found", name, id)
		}
		return "", errors.Wrapf(err, "error reading credential spec %q from registry for container %s", name, id)
	}

	return value, 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 := os.ReadFile(full)
	if err != nil {
		return "", errors.Wrapf(err, "credential spec for container %s could not be read from file %q", id, full)
	}
	return string(bcontents[:]), nil
}

func setupWindowsDevices(devices []containertypes.DeviceMapping) (specDevices []specs.WindowsDevice, err error) {
	if len(devices) == 0 {
		return
	}

	for _, deviceMapping := range devices {
		devicePath := deviceMapping.PathOnHost
		if strings.HasPrefix(devicePath, "class/") {
			devicePath = strings.Replace(devicePath, "class/", "class://", 1)
		}

		srcParts := strings.SplitN(devicePath, "://", 2)
		if len(srcParts) != 2 {
			return nil, errors.Errorf("invalid device assignment path: '%s', must be 'class/ID' or 'IDType://ID'", deviceMapping.PathOnHost)
		}
		if srcParts[0] == "" {
			return nil, errors.Errorf("invalid device assignment path: '%s', IDType cannot be empty", deviceMapping.PathOnHost)
		}
		wd := specs.WindowsDevice{
			ID:     srcParts[1],
			IDType: srcParts[0],
		}
		specDevices = append(specDevices, wd)
	}

	return
}

A container's HostConfig is serialized to disk so it's possible for these fields to be set on an existing Windows container. Such containers will fail to start until the container configuration is updated to zero out the offending fields. Since the presence of these fields is otherwise harmless, we should probably allow existing containers to start even if these fields are set.