diff --git a/daemon/monitor_windows.go b/daemon/monitor_windows.go index 9648b1b415..15d656de0e 100644 --- a/daemon/monitor_windows.go +++ b/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) } diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 555a466fe9..0254351569 100644 --- a/daemon/oci_windows.go +++ b/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 +} diff --git a/daemon/start_windows.go b/daemon/start_windows.go index 098380d00b..9082a93ff6 100644 --- a/daemon/start_windows.go +++ b/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 -} diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index db84061254..8f02bf453a 100644 --- a/libcontainerd/client_windows.go +++ b/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 } - // 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.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 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") } - // LayerFolderPath (writeable layer) + Layers (Guid + path) - configuration.LayerFolderPath = layerOpt.LayerFolderPath - for _, layerPath := range layerOpt.LayerPaths { + // 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") + } + + // 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() diff --git a/libcontainerd/container_windows.go b/libcontainerd/container_windows.go index 33480514e5..06f9c82209 100644 --- a/libcontainerd/container_windows.go +++ b/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. - createProcessParms := &hcsshim.ProcessConfig{ - EmulateConsole: ctr.ociSpec.Process.Terminal, - WorkingDirectory: ctr.ociSpec.Process.Cwd, - CreateStdInPipe: !isServicing, - CreateStdOutPipe: !isServicing, - CreateStdErrPipe: !ctr.ociSpec.Process.Terminal && !isServicing, + 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: emulateConsole, + WorkingDirectory: ctr.ociSpec.Process.Cwd, + 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) diff --git a/libcontainerd/types_windows.go b/libcontainerd/types_windows.go index 1ffe0f40aa..f271ecd479 100644 --- a/libcontainerd/types_windows.go +++ b/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 diff --git a/libcontainerd/utils_windows.go b/libcontainerd/utils_windows.go index e741a296f0..aa2fe422a6 100644 --- a/libcontainerd/utils_windows.go +++ b/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 diff --git a/oci/defaults.go b/oci/defaults.go index 0d77544310..a75d3402fb 100644 --- a/oci/defaults.go +++ b/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")