Browse Source

Update Windows and LCOW to use v1.0.0 runtime-spec

Signed-off-by: Darren Stahl <darst@microsoft.com>
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
Darren Stahl 8 years ago
parent
commit
7c29103ad9

+ 3 - 8
daemon/monitor_windows.go

@@ -22,22 +22,17 @@ func (daemon *Daemon) postRunProcessing(container *container.Container, e libcon
 			return err
 		}
 
-		newOpts := []libcontainerd.CreateOption{&libcontainerd.ServicingOption{
-			IsServicing: true,
-		}}
+		// Turn on servicing
+		spec.Windows.Servicing = true
 
 		copts, err := daemon.getLibcontainerdCreateOptions(container)
 		if err != nil {
 			return err
 		}
 
-		if copts != nil {
-			newOpts = append(newOpts, copts...)
-		}
-
 		// Create a new servicing container, which will start, complete the update, and merge back the
 		// results if it succeeded, all as part of the below function call.
-		if err := daemon.containerd.Create((container.ID + "_servicing"), "", "", *spec, container.InitializeStdio, newOpts...); err != nil {
+		if err := daemon.containerd.Create((container.ID + "_servicing"), "", "", *spec, container.InitializeStdio, copts...); err != nil {
 			container.SetExitCode(-1)
 			return fmt.Errorf("Post-run update servicing failed: %s", err)
 		}

+ 203 - 4
daemon/oci_windows.go

@@ -1,13 +1,25 @@
 package daemon
 
 import (
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/container"
+	"github.com/docker/docker/layer"
 	"github.com/docker/docker/oci"
 	"github.com/docker/docker/pkg/sysinfo"
 	"github.com/docker/docker/pkg/system"
 	"github.com/opencontainers/runtime-spec/specs-go"
 	"golang.org/x/sys/windows"
+	"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) {
@@ -53,6 +65,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		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
@@ -105,13 +121,93 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 	s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv)
 	if c.Config.Tty {
 		s.Process.Terminal = c.Config.Tty
-		s.Process.ConsoleSize.Height = c.HostConfig.ConsoleSize[0]
-		s.Process.ConsoleSize.Width = c.HostConfig.ConsoleSize[1]
+		s.Process.ConsoleSize = &specs.Box{
+			Height: c.HostConfig.ConsoleSize[0],
+			Width:  c.HostConfig.ConsoleSize[1],
+		}
 	}
 	s.Process.User.Username = c.Config.User
 
+	// Get the layer path for each layer.
+	max := len(img.RootFS.DiffIDs)
+	for i := 1; i <= max; i++ {
+		img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i]
+		layerPath, err := layer.GetLayerPath(daemon.stores[c.Platform].layerStore, img.RootFS.ChainID())
+		if err != nil {
+			return nil, fmt.Errorf("failed to get layer path from graphdriver %s for ImageID %s - %s", daemon.stores[c.Platform].layerStore, img.RootFS.ChainID(), err)
+		}
+		// Reverse order, expecting parent most first
+		s.Windows.LayerFolders = append([]string{layerPath}, s.Windows.LayerFolders...)
+	}
+	m, err := c.RWLayer.Metadata()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get layer metadata - %s", err)
+	}
+	s.Windows.LayerFolders = append(s.Windows.LayerFolders, m["dir"])
+
+	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 := c.GetEndpointInNetwork(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 img.OS == "windows" {
-		daemon.createSpecWindowsFields(c, &s, isHyperV)
+		if err := daemon.createSpecWindowsFields(c, &s, isHyperV); err != nil {
+			return nil, err
+		}
 	} else {
 		// TODO @jhowardmsft LCOW Support. Modify this check when running in dual-mode
 		if system.LCOWSupported() && img.OS == "linux" {
@@ -123,7 +219,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 }
 
 // Sets the Windows-specific fields of the OCI spec
-func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) {
+func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) error {
 	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
@@ -138,8 +234,14 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
 	s.Root.Readonly = false // Windows does not support a read-only root filesystem
 	if !isHyperV {
 		s.Root.Path = c.BaseFS // 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
+
 	// In s.Windows.Resources
 	cpuShares := uint16(c.HostConfig.CPUShares)
 	cpuMaximum := uint16(c.HostConfig.CPUPercent) * 100
@@ -179,6 +281,54 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
 			Iops: &c.HostConfig.IOMaximumIOps,
 		},
 	}
+
+	// Read and add credentials from the security options if a credential spec has been provided.
+	if c.HostConfig.SecurityOpt != nil {
+		cs := ""
+		for _, sOpt := range c.HostConfig.SecurityOpt {
+			sOpt = strings.ToLower(sOpt)
+			if !strings.Contains(sOpt, "=") {
+				return 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 fmt.Errorf("invalid security option: %s", sOpt)
+			}
+			if splitsOpt[0] != "credentialspec" {
+				return fmt.Errorf("security option not supported: %s", splitsOpt[0])
+			}
+
+			var (
+				match   bool
+				csValue string
+				err     error
+			)
+			if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match {
+				if csValue == "" {
+					return fmt.Errorf("no value supplied for file:// credential spec security option")
+				}
+				if cs, err = readCredentialSpecFile(c.ID, daemon.root, filepath.Clean(csValue)); err != nil {
+					return err
+				}
+			} else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match {
+				if csValue == "" {
+					return fmt.Errorf("no value supplied for registry:// credential spec security option")
+				}
+				if cs, err = readCredentialSpecRegistry(c.ID, csValue); err != nil {
+					return err
+				}
+			} else {
+				return fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
+			}
+		}
+		s.Windows.CredentialSpec = cs
+	}
+
+	// Assume we are not starting a container for a servicing operation
+	s.Windows.Servicing = false
+
+	return nil
 }
 
 // Sets the Linux-specific fields of the OCI spec
@@ -205,3 +355,52 @@ func escapeArgs(args []string) []string {
 func (daemon *Daemon) mergeUlimits(c *containertypes.HostConfig) {
 	return
 }
+
+// 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
+}

+ 0 - 202
daemon/start_windows.go

@@ -1,148 +1,14 @@
 package daemon
 
 import (
-	"fmt"
-	"io/ioutil"
-	"path/filepath"
-	"strings"
-
 	"github.com/Microsoft/opengcs/client"
 	"github.com/docker/docker/container"
-	"github.com/docker/docker/layer"
 	"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) {
 	createOptions := []libcontainerd.CreateOption{}
 
-	// Are we going to run as a Hyper-V container?
-	hvOpts := &libcontainerd.HyperVIsolationOption{}
-	if container.HostConfig.Isolation.IsDefault() {
-		// Container is set to use the default, so take the default from the daemon configuration
-		hvOpts.IsHyperV = daemon.defaultIsolation.IsHyperV()
-	} else {
-		// Container is requesting an isolation mode. Honour it.
-		hvOpts.IsHyperV = container.HostConfig.Isolation.IsHyperV()
-	}
-
-	dnsSearch := daemon.getDNSSearchSettings(container)
-
-	// Generate the layer folder of the layer options
-	layerOpts := &libcontainerd.LayerOption{}
-	m, err := container.RWLayer.Metadata()
-	if err != nil {
-		return nil, fmt.Errorf("failed to get layer metadata - %s", err)
-	}
-	layerOpts.LayerFolderPath = m["dir"]
-
-	// Generate the layer paths of the layer options
-	img, err := daemon.stores[container.Platform].imageStore.Get(container.ImageID)
-	if err != nil {
-		return nil, fmt.Errorf("failed to graph.Get on ImageID %s - %s", container.ImageID, err)
-	}
-	// Get the layer path for each layer.
-	max := len(img.RootFS.DiffIDs)
-	for i := 1; i <= max; i++ {
-		img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i]
-		layerPath, err := layer.GetLayerPath(daemon.stores[container.Platform].layerStore, img.RootFS.ChainID())
-		if err != nil {
-			return nil, fmt.Errorf("failed to get layer path from graphdriver %s for ImageID %s - %s", daemon.stores[container.Platform].layerStore, img.RootFS.ChainID(), err)
-		}
-		// Reverse order, expecting parent most first
-		layerOpts.LayerPaths = append([]string{layerPath}, layerOpts.LayerPaths...)
-	}
-
-	// Get endpoints for the libnetwork allocated networks to the container
-	var epList []string
-	AllowUnqualifiedDNSQuery := false
-	gwHNSID := ""
-	if container.NetworkSettings != nil {
-		for n := range container.NetworkSettings.Networks {
-			sn, err := daemon.FindNetwork(n)
-			if err != nil {
-				continue
-			}
-
-			ep, err := container.GetEndpointInNetwork(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
-			}
-		}
-	}
-
-	if gwHNSID != "" {
-		epList = append(epList, gwHNSID)
-	}
-
-	// 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)
-		}
-	}
-
 	// LCOW options.
 	if container.Platform == "linux" {
 		config := &client.Config{}
@@ -173,73 +39,5 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain
 		createOptions = append(createOptions, lcowOpts)
 	}
 
-	// Now add the remaining options.
-	createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore})
-	createOptions = append(createOptions, hvOpts)
-	createOptions = append(createOptions, layerOpts)
-
-	var networkSharedContainerID string
-	if container.HostConfig.NetworkMode.IsContainer() {
-		networkSharedContainerID = container.NetworkSharedContainerID
-		for _, ep := range container.SharedEndpointList {
-			epList = append(epList, ep)
-		}
-	}
-
-	createOptions = append(createOptions, &libcontainerd.NetworkEndpointsOption{
-		Endpoints:                epList,
-		AllowUnqualifiedDNSQuery: AllowUnqualifiedDNSQuery,
-		DNSSearchList:            dnsSearch,
-		NetworkSharedContainerID: networkSharedContainerID,
-	})
 	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
-}

+ 84 - 78
libcontainerd/client_windows.go

@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"syscall"
 	"time"
@@ -102,8 +103,11 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir
 	if b, err := json.Marshal(spec); err == nil {
 		logrus.Debugln("libcontainerd: client.Create() with spec", string(b))
 	}
-	osName := spec.Platform.OS
-	if osName == "windows" {
+
+	// spec.Linux must be nil for Windows containers, but spec.Windows will be filled in regardless of container platform.
+	// This is a temporary workaround due to LCOW requiring layer folder paths, which are stored under spec.Windows.
+	// TODO: @darrenstahlmsft fix this once the OCI spec is updated to support layer folder paths for LCOW
+	if spec.Linux == nil {
 		return clnt.createWindows(containerID, checkpoint, checkpointDir, spec, attachStdio, options...)
 	}
 	return clnt.createLinux(containerID, checkpoint, checkpointDir, spec, attachStdio, options...)
@@ -114,9 +118,10 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 		SystemType: "Container",
 		Name:       containerID,
 		Owner:      defaultOwner,
-		IgnoreFlushesDuringBoot: false,
+		IgnoreFlushesDuringBoot: spec.Windows.IgnoreFlushesDuringBoot,
 		HostName:                spec.Hostname,
 		HvPartition:             false,
+		Servicing:               spec.Windows.Servicing,
 	}
 
 	if spec.Windows.Resources != nil {
@@ -155,49 +160,43 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 		}
 	}
 
-	var layerOpt *LayerOption
-	for _, option := range options {
-		if s, ok := option.(*ServicingOption); ok {
-			configuration.Servicing = s.IsServicing
-			continue
-		}
-		if f, ok := option.(*FlushOption); ok {
-			configuration.IgnoreFlushesDuringBoot = f.IgnoreFlushesDuringBoot
-			continue
-		}
-		if h, ok := option.(*HyperVIsolationOption); ok {
-			configuration.HvPartition = h.IsHyperV
-			continue
-		}
-		if l, ok := option.(*LayerOption); ok {
-			layerOpt = l
-		}
-		if n, ok := option.(*NetworkEndpointsOption); ok {
-			configuration.EndpointList = n.Endpoints
-			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
-			if n.DNSSearchList != nil {
-				configuration.DNSSearchList = strings.Join(n.DNSSearchList, ",")
-			}
-			configuration.NetworkSharedContainerName = n.NetworkSharedContainerID
-			continue
-		}
-		if c, ok := option.(*CredentialsOption); ok {
-			configuration.Credentials = c.Credentials
-			continue
+	if spec.Windows.HyperV != nil {
+		configuration.HvPartition = true
+	}
+
+	if spec.Windows.Network != nil {
+		configuration.EndpointList = spec.Windows.Network.EndpointList
+		configuration.AllowUnqualifiedDNSQuery = spec.Windows.Network.AllowUnqualifiedDNSQuery
+		if spec.Windows.Network.DNSSearchList != nil {
+			configuration.DNSSearchList = strings.Join(spec.Windows.Network.DNSSearchList, ",")
 		}
+		configuration.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName
+	}
+
+	if cs, ok := spec.Windows.CredentialSpec.(string); ok {
+		configuration.Credentials = cs
 	}
 
-	// We must have a layer option with at least one path
-	if layerOpt == nil || layerOpt.LayerPaths == nil {
-		return fmt.Errorf("no layer option or paths were supplied to the runtime")
+	// We must have least two layers in the spec, the bottom one being a base image,
+	// the top one being the RW layer.
+	if spec.Windows.LayerFolders == nil || len(spec.Windows.LayerFolders) < 2 {
+		return fmt.Errorf("OCI spec is invalid - at least two LayerFolders must be supplied to the runtime")
 	}
 
+	// Strip off the top-most layer as that's passed in separately to HCS
+	configuration.LayerFolderPath = spec.Windows.LayerFolders[len(spec.Windows.LayerFolders)-1]
+	layerFolders := spec.Windows.LayerFolders[:len(spec.Windows.LayerFolders)-1]
+
 	if configuration.HvPartition {
-		// Find the upper-most utility VM image, since the utility VM does not
-		// use layering in RS1.
-		// TODO @swernli/jhowardmsft at some point post RS1 this may be re-locatable.
+		// We don't currently support setting the utility VM image explicitly.
+		// TODO @swernli/jhowardmsft circa RS3/4, this may be re-locatable.
+		if spec.Windows.HyperV.UtilityVMPath != "" {
+			return errors.New("runtime does not support an explicit utility VM path for Hyper-V containers")
+		}
+
+		// Find the upper-most utility VM image.
 		var uvmImagePath string
-		for _, path := range layerOpt.LayerPaths {
+		for _, path := range layerFolders {
 			fullPath := filepath.Join(path, "UtilityVM")
 			_, err := os.Stat(fullPath)
 			if err == nil {
@@ -212,13 +211,24 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 			return errors.New("utility VM image could not be found")
 		}
 		configuration.HvRuntime = &hcsshim.HvRuntime{ImagePath: uvmImagePath}
+
+		if spec.Root.Path != "" {
+			return errors.New("OCI spec is invalid - Root.Path must be omitted for a Hyper-V container")
+		}
 	} else {
-		configuration.VolumePath = spec.Root.Path
+		const volumeGUIDRegex = `^\\\\\?\\(Volume)\{{0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}\}\\$`
+		if _, err := regexp.MatchString(volumeGUIDRegex, spec.Root.Path); err != nil {
+			return fmt.Errorf(`OCI spec is invalid - Root.Path '%s' must be a volume GUID path in the format '\\?\Volume{GUID}\'`, spec.Root.Path)
+		}
+		// HCS API requires the trailing backslash to be removed
+		configuration.VolumePath = spec.Root.Path[:len(spec.Root.Path)-1]
 	}
 
-	configuration.LayerFolderPath = layerOpt.LayerFolderPath
+	if spec.Root.Readonly {
+		return errors.New(`OCI spec is invalid - Root.Readonly must not be set on Windows`)
+	}
 
-	for _, layerPath := range layerOpt.LayerPaths {
+	for _, layerPath := range layerFolders {
 		_, filename := filepath.Split(layerPath)
 		g, err := hcsshim.NameToGuid(filename)
 		if err != nil {
@@ -235,6 +245,9 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 	var mps []hcsshim.MappedPipe
 	for _, mount := range spec.Mounts {
 		const pipePrefix = `\\.\pipe\`
+		if mount.Type != "" {
+			return fmt.Errorf("OCI spec is invalid - Mount.Type '%s' must not be set", mount.Type)
+		}
 		if strings.HasPrefix(mount.Destination, pipePrefix) {
 			mp := hcsshim.MappedPipe{
 				HostPath:          mount.Source,
@@ -278,6 +291,7 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 			},
 			processes: make(map[string]*process),
 		},
+		isWindows:    true,
 		ociSpec:      spec,
 		hcsContainer: hcsContainer,
 	}
@@ -306,12 +320,8 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
 func (clnt *client) createLinux(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error {
 	logrus.Debugf("libcontainerd: createLinux(): containerId %s ", containerID)
 
-	var layerOpt *LayerOption
 	var lcowOpt *LCOWOption
 	for _, option := range options {
-		if layer, ok := option.(*LayerOption); ok {
-			layerOpt = layer
-		}
 		if lcow, ok := option.(*LCOWOption); ok {
 			lcowOpt = lcow
 		}
@@ -342,14 +352,20 @@ func (clnt *client) createLinux(containerID string, checkpoint string, checkpoin
 		}
 	}
 
-	// We must have a layer option with at least one path
-	if layerOpt == nil || layerOpt.LayerPaths == nil {
-		return fmt.Errorf("no layer option or paths were supplied to the runtime")
+	if spec.Windows == nil {
+		return fmt.Errorf("spec.Windows must not be nil for LCOW containers")
+	}
+
+	// We must have least one layer in the spec
+	if spec.Windows.LayerFolders == nil || len(spec.Windows.LayerFolders) == 0 {
+		return fmt.Errorf("OCI spec is invalid - at least one LayerFolders must be supplied to the runtime")
 	}
 
-	// LayerFolderPath (writeable layer) + Layers (Guid + path)
-	configuration.LayerFolderPath = layerOpt.LayerFolderPath
-	for _, layerPath := range layerOpt.LayerPaths {
+	// Strip off the top-most layer as that's passed in separately to HCS
+	configuration.LayerFolderPath = spec.Windows.LayerFolders[len(spec.Windows.LayerFolders)-1]
+	layerFolders := spec.Windows.LayerFolders[:len(spec.Windows.LayerFolders)-1]
+
+	for _, layerPath := range layerFolders {
 		_, filename := filepath.Split(layerPath)
 		g, err := hcsshim.NameToGuid(filename)
 		if err != nil {
@@ -361,16 +377,13 @@ func (clnt *client) createLinux(containerID string, checkpoint string, checkpoin
 		})
 	}
 
-	for _, option := range options {
-		if n, ok := option.(*NetworkEndpointsOption); ok {
-			configuration.EndpointList = n.Endpoints
-			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
-			if n.DNSSearchList != nil {
-				configuration.DNSSearchList = strings.Join(n.DNSSearchList, ",")
-			}
-			configuration.NetworkSharedContainerName = n.NetworkSharedContainerID
-			break
+	if spec.Windows.Network != nil {
+		configuration.EndpointList = spec.Windows.Network.EndpointList
+		configuration.AllowUnqualifiedDNSQuery = spec.Windows.Network.AllowUnqualifiedDNSQuery
+		if spec.Windows.Network.DNSSearchList != nil {
+			configuration.DNSSearchList = strings.Join(spec.Windows.Network.DNSSearchList, ",")
 		}
+		configuration.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName
 	}
 
 	hcsContainer, err := hcsshim.CreateContainer(containerID, configuration)
@@ -436,8 +449,10 @@ func (clnt *client) AddProcess(ctx context.Context, containerID, processFriendly
 	}
 	if procToAdd.Terminal {
 		createProcessParms.EmulateConsole = true
-		createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height)
-		createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width)
+		if procToAdd.ConsoleSize != nil {
+			createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height)
+			createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width)
+		}
 	}
 
 	// Take working directory from the process to add if it is defined,
@@ -450,7 +465,7 @@ func (clnt *client) AddProcess(ctx context.Context, containerID, processFriendly
 
 	// Configure the environment for the process
 	createProcessParms.Environment = setupEnvironmentVariables(procToAdd.Env)
-	if container.ociSpec.Platform.OS == "windows" {
+	if container.isWindows {
 		createProcessParms.CommandLine = strings.Join(procToAdd.Args, " ")
 	} else {
 		createProcessParms.CommandArgs = procToAdd.Args
@@ -614,13 +629,8 @@ func (clnt *client) Pause(containerID string) error {
 		return err
 	}
 
-	for _, option := range container.options {
-		if h, ok := option.(*HyperVIsolationOption); ok {
-			if !h.IsHyperV {
-				return errors.New("cannot pause Windows Server Containers")
-			}
-			break
-		}
+	if container.ociSpec.Windows.HyperV == nil {
+		return errors.New("cannot pause Windows Server Containers")
 	}
 
 	err = container.hcsContainer.Pause()
@@ -654,13 +664,9 @@ func (clnt *client) Resume(containerID string) error {
 	}
 
 	// This should never happen, since Windows Server Containers cannot be paused
-	for _, option := range container.options {
-		if h, ok := option.(*HyperVIsolationOption); ok {
-			if !h.IsHyperV {
-				return errors.New("cannot resume Windows Server Containers")
-			}
-			break
-		}
+
+	if container.ociSpec.Windows.HyperV == nil {
+		return errors.New("cannot resume Windows Server Containers")
 	}
 
 	err = container.hcsContainer.Resume()

+ 24 - 18
libcontainerd/container_windows.go

@@ -25,6 +25,7 @@ type container struct {
 	// otherwise have access to the Spec
 	ociSpec specs.Spec
 
+	isWindows           bool
 	manualStopRequested bool
 	hcsContainer        hcsshim.Container
 }
@@ -43,13 +44,6 @@ func (ctr *container) newProcess(friendlyName string) *process {
 // Caller needs to lock container ID before calling this method.
 func (ctr *container) start(attachStdio StdioCallback) error {
 	var err error
-	isServicing := false
-
-	for _, option := range ctr.options {
-		if s, ok := option.(*ServicingOption); ok && s.IsServicing {
-			isServicing = true
-		}
-	}
 
 	// Start the container.  If this is a servicing container, this call will block
 	// until the container is done with the servicing execution.
@@ -69,27 +63,39 @@ func (ctr *container) start(attachStdio StdioCallback) error {
 	// docker can always grab the output through logs. We also tell HCS to always
 	// create stdin, even if it's not used - it will be closed shortly. Stderr
 	// is only created if it we're not -t.
+	var (
+		emulateConsole   bool
+		createStdErrPipe bool
+	)
+	if ctr.ociSpec.Process != nil {
+		emulateConsole = ctr.ociSpec.Process.Terminal
+		createStdErrPipe = !ctr.ociSpec.Process.Terminal && !ctr.ociSpec.Windows.Servicing
+	}
+
 	createProcessParms := &hcsshim.ProcessConfig{
-		EmulateConsole:   ctr.ociSpec.Process.Terminal,
+		EmulateConsole:   emulateConsole,
 		WorkingDirectory: ctr.ociSpec.Process.Cwd,
-		CreateStdInPipe:  !isServicing,
-		CreateStdOutPipe: !isServicing,
-		CreateStdErrPipe: !ctr.ociSpec.Process.Terminal && !isServicing,
+		CreateStdInPipe:  !ctr.ociSpec.Windows.Servicing,
+		CreateStdOutPipe: !ctr.ociSpec.Windows.Servicing,
+		CreateStdErrPipe: createStdErrPipe,
+	}
+
+	if ctr.ociSpec.Process != nil && ctr.ociSpec.Process.ConsoleSize != nil {
+		createProcessParms.ConsoleSize[0] = uint(ctr.ociSpec.Process.ConsoleSize.Height)
+		createProcessParms.ConsoleSize[1] = uint(ctr.ociSpec.Process.ConsoleSize.Width)
 	}
-	createProcessParms.ConsoleSize[0] = uint(ctr.ociSpec.Process.ConsoleSize.Height)
-	createProcessParms.ConsoleSize[1] = uint(ctr.ociSpec.Process.ConsoleSize.Width)
 
 	// Configure the environment for the process
 	createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env)
-	if ctr.ociSpec.Platform.OS == "windows" {
+	if ctr.isWindows {
 		createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ")
 	} else {
 		createProcessParms.CommandArgs = ctr.ociSpec.Process.Args
 	}
 	createProcessParms.User = ctr.ociSpec.Process.User.Username
 
-	// Linux containers requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM.
-	if ctr.ociSpec.Platform.OS == "linux" {
+	// LCOW requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM.
+	if !ctr.isWindows {
 		ociBuf, err := json.Marshal(ctr.ociSpec)
 		if err != nil {
 			return err
@@ -118,7 +124,7 @@ func (ctr *container) start(attachStdio StdioCallback) error {
 
 	// If this is a servicing container, wait on the process synchronously here and
 	// if it succeeds, wait for it cleanly shutdown and merge into the parent container.
-	if isServicing {
+	if ctr.ociSpec.Windows.Servicing {
 		exitCode := ctr.waitProcessExitCode(&ctr.process)
 
 		if exitCode != 0 {
@@ -244,7 +250,7 @@ func (ctr *container) waitExit(process *process, isFirstProcessToStart bool) err
 		si.State = StateExitProcess
 	} else {
 		// Pending updates is only applicable for WCOW
-		if ctr.ociSpec.Platform.OS == "windows" {
+		if ctr.isWindows {
 			updatePending, err := ctr.hcsContainer.HasPendingUpdates()
 			if err != nil {
 				logrus.Warnf("libcontainerd: HasPendingUpdates() failed (container may have been killed): %s", err)

+ 0 - 43
libcontainerd/types_windows.go

@@ -31,49 +31,6 @@ type LCOWOption struct {
 	Config *opengcs.Config
 }
 
-// ServicingOption is a CreateOption with a no-op application that signifies
-// the container needs to be used for a Windows servicing operation.
-type ServicingOption struct {
-	IsServicing bool
-}
-
-// FlushOption is a CreateOption that signifies if the container should be
-// started with flushes ignored until boot has completed. This is an optimisation
-// for first boot of a container.
-type FlushOption struct {
-	IgnoreFlushesDuringBoot bool
-}
-
-// HyperVIsolationOption is a CreateOption that indicates whether the runtime
-// should start the container as a Hyper-V container.
-type HyperVIsolationOption struct {
-	IsHyperV bool
-}
-
-// LayerOption is a CreateOption that indicates to the runtime the layer folder
-// and layer paths for a container.
-type LayerOption struct {
-	// LayerFolderPath is the path to the current layer folder. Empty for Hyper-V containers.
-	LayerFolderPath string `json:",omitempty"`
-	// Layer paths of the parent layers
-	LayerPaths []string
-}
-
-// NetworkEndpointsOption is a CreateOption that provides the runtime list
-// of network endpoints to which a container should be attached during its creation.
-type NetworkEndpointsOption struct {
-	Endpoints                []string
-	AllowUnqualifiedDNSQuery bool
-	DNSSearchList            []string
-	NetworkSharedContainerID string
-}
-
-// 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)
 type Checkpoint struct {
 	Name string

+ 0 - 30
libcontainerd/utils_windows.go

@@ -15,36 +15,6 @@ func setupEnvironmentVariables(a []string) map[string]string {
 	return r
 }
 
-// Apply for a servicing option is a no-op.
-func (s *ServicingOption) Apply(interface{}) error {
-	return nil
-}
-
-// Apply for the flush option is a no-op.
-func (f *FlushOption) Apply(interface{}) error {
-	return nil
-}
-
-// Apply for the hypervisolation option is a no-op.
-func (h *HyperVIsolationOption) Apply(interface{}) error {
-	return nil
-}
-
-// Apply for the layer option is a no-op.
-func (h *LayerOption) Apply(interface{}) error {
-	return nil
-}
-
-// Apply for the network endpoints option is a no-op.
-func (s *NetworkEndpointsOption) Apply(interface{}) error {
-	return nil
-}
-
-// Apply for the credentials option is a no-op.
-func (s *CredentialsOption) Apply(interface{}) error {
-	return nil
-}
-
 // Apply for the LCOW option is a no-op.
 func (s *LCOWOption) Apply(interface{}) error {
 	return nil

+ 15 - 5
oci/defaults.go

@@ -51,6 +51,8 @@ func DefaultWindowsSpec() specs.Spec {
 	return specs.Spec{
 		Version: specs.Version,
 		Windows: &specs.Windows{},
+		Process: &specs.Process{},
+		Root:    &specs.Root{},
 	}
 }
 
@@ -68,6 +70,7 @@ func DefaultLinuxSpec() specs.Spec {
 	s := specs.Spec{
 		Version: specs.Version,
 		Process: &specs.Process{},
+		Root:    &specs.Root{},
 	}
 	s.Mounts = []specs.Mount{
 		{
@@ -113,11 +116,13 @@ func DefaultLinuxSpec() specs.Spec {
 			Options:     []string{"nosuid", "noexec", "nodev", "mode=1777"},
 		},
 	}
-	s.Process.Capabilities = &specs.LinuxCapabilities{
-		Bounding:    defaultCapabilities(),
-		Permitted:   defaultCapabilities(),
-		Inheritable: defaultCapabilities(),
-		Effective:   defaultCapabilities(),
+	s.Process = &specs.Process{
+		Capabilities: &specs.LinuxCapabilities{
+			Bounding:    defaultCapabilities(),
+			Permitted:   defaultCapabilities(),
+			Inheritable: defaultCapabilities(),
+			Effective:   defaultCapabilities(),
+		},
 	}
 
 	s.Linux = &specs.Linux{
@@ -207,6 +212,11 @@ func DefaultLinuxSpec() specs.Spec {
 		},
 	}
 
+	// For LCOW support, populate a blank Windows spec
+	if runtime.GOOS == "windows" {
+		s.Windows = &specs.Windows{}
+	}
+
 	// For LCOW support, don't mask /sys/firmware
 	if runtime.GOOS != "windows" {
 		s.Linux.MaskedPaths = append(s.Linux.MaskedPaths, "/sys/firmware")