Browse code

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 authored on 2017/08/02 02:00:38
Showing 8 changed files
... ...
@@ -22,22 +22,17 @@ func (daemon *Daemon) postRunProcessing(container *container.Container, e libcon
22 22
 			return err
23 23
 		}
24 24
 
25
-		newOpts := []libcontainerd.CreateOption{&libcontainerd.ServicingOption{
26
-			IsServicing: true,
27
-		}}
25
+		// Turn on servicing
26
+		spec.Windows.Servicing = true
28 27
 
29 28
 		copts, err := daemon.getLibcontainerdCreateOptions(container)
30 29
 		if err != nil {
31 30
 			return err
32 31
 		}
33 32
 
34
-		if copts != nil {
35
-			newOpts = append(newOpts, copts...)
36
-		}
37
-
38 33
 		// Create a new servicing container, which will start, complete the update, and merge back the
39 34
 		// results if it succeeded, all as part of the below function call.
40
-		if err := daemon.containerd.Create((container.ID + "_servicing"), "", "", *spec, container.InitializeStdio, newOpts...); err != nil {
35
+		if err := daemon.containerd.Create((container.ID + "_servicing"), "", "", *spec, container.InitializeStdio, copts...); err != nil {
41 36
 			container.SetExitCode(-1)
42 37
 			return fmt.Errorf("Post-run update servicing failed: %s", err)
43 38
 		}
... ...
@@ -1,13 +1,25 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"fmt"
5
+	"io/ioutil"
6
+	"path/filepath"
7
+	"strings"
8
+
4 9
 	containertypes "github.com/docker/docker/api/types/container"
5 10
 	"github.com/docker/docker/container"
11
+	"github.com/docker/docker/layer"
6 12
 	"github.com/docker/docker/oci"
7 13
 	"github.com/docker/docker/pkg/sysinfo"
8 14
 	"github.com/docker/docker/pkg/system"
9 15
 	"github.com/opencontainers/runtime-spec/specs-go"
10 16
 	"golang.org/x/sys/windows"
17
+	"golang.org/x/sys/windows/registry"
18
+)
19
+
20
+const (
21
+	credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
22
+	credentialSpecFileLocation     = "CredentialSpecs"
11 23
 )
12 24
 
13 25
 func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
... ...
@@ -53,6 +65,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
53 53
 		isHyperV = c.HostConfig.Isolation.IsHyperV()
54 54
 	}
55 55
 
56
+	if isHyperV {
57
+		s.Windows.HyperV = &specs.WindowsHyperV{}
58
+	}
59
+
56 60
 	// If the container has not been started, and has configs or secrets
57 61
 	// secrets, create symlinks to each config and secret. If it has been
58 62
 	// 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) {
105 105
 	s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv)
106 106
 	if c.Config.Tty {
107 107
 		s.Process.Terminal = c.Config.Tty
108
-		s.Process.ConsoleSize.Height = c.HostConfig.ConsoleSize[0]
109
-		s.Process.ConsoleSize.Width = c.HostConfig.ConsoleSize[1]
108
+		s.Process.ConsoleSize = &specs.Box{
109
+			Height: c.HostConfig.ConsoleSize[0],
110
+			Width:  c.HostConfig.ConsoleSize[1],
111
+		}
110 112
 	}
111 113
 	s.Process.User.Username = c.Config.User
112 114
 
115
+	// Get the layer path for each layer.
116
+	max := len(img.RootFS.DiffIDs)
117
+	for i := 1; i <= max; i++ {
118
+		img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i]
119
+		layerPath, err := layer.GetLayerPath(daemon.stores[c.Platform].layerStore, img.RootFS.ChainID())
120
+		if err != nil {
121
+			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)
122
+		}
123
+		// Reverse order, expecting parent most first
124
+		s.Windows.LayerFolders = append([]string{layerPath}, s.Windows.LayerFolders...)
125
+	}
126
+	m, err := c.RWLayer.Metadata()
127
+	if err != nil {
128
+		return nil, fmt.Errorf("failed to get layer metadata - %s", err)
129
+	}
130
+	s.Windows.LayerFolders = append(s.Windows.LayerFolders, m["dir"])
131
+
132
+	dnsSearch := daemon.getDNSSearchSettings(c)
133
+
134
+	// Get endpoints for the libnetwork allocated networks to the container
135
+	var epList []string
136
+	AllowUnqualifiedDNSQuery := false
137
+	gwHNSID := ""
138
+	if c.NetworkSettings != nil {
139
+		for n := range c.NetworkSettings.Networks {
140
+			sn, err := daemon.FindNetwork(n)
141
+			if err != nil {
142
+				continue
143
+			}
144
+
145
+			ep, err := c.GetEndpointInNetwork(sn)
146
+			if err != nil {
147
+				continue
148
+			}
149
+
150
+			data, err := ep.DriverInfo()
151
+			if err != nil {
152
+				continue
153
+			}
154
+
155
+			if data["GW_INFO"] != nil {
156
+				gwInfo := data["GW_INFO"].(map[string]interface{})
157
+				if gwInfo["hnsid"] != nil {
158
+					gwHNSID = gwInfo["hnsid"].(string)
159
+				}
160
+			}
161
+
162
+			if data["hnsid"] != nil {
163
+				epList = append(epList, data["hnsid"].(string))
164
+			}
165
+
166
+			if data["AllowUnqualifiedDNSQuery"] != nil {
167
+				AllowUnqualifiedDNSQuery = true
168
+			}
169
+		}
170
+	}
171
+
172
+	var networkSharedContainerID string
173
+	if c.HostConfig.NetworkMode.IsContainer() {
174
+		networkSharedContainerID = c.NetworkSharedContainerID
175
+		for _, ep := range c.SharedEndpointList {
176
+			epList = append(epList, ep)
177
+		}
178
+	}
179
+
180
+	if gwHNSID != "" {
181
+		epList = append(epList, gwHNSID)
182
+	}
183
+
184
+	s.Windows.Network = &specs.WindowsNetwork{
185
+		AllowUnqualifiedDNSQuery:   AllowUnqualifiedDNSQuery,
186
+		DNSSearchList:              dnsSearch,
187
+		EndpointList:               epList,
188
+		NetworkSharedContainerName: networkSharedContainerID,
189
+	}
190
+
113 191
 	if img.OS == "windows" {
114
-		daemon.createSpecWindowsFields(c, &s, isHyperV)
192
+		if err := daemon.createSpecWindowsFields(c, &s, isHyperV); err != nil {
193
+			return nil, err
194
+		}
115 195
 	} else {
116 196
 		// TODO @jhowardmsft LCOW Support. Modify this check when running in dual-mode
117 197
 		if system.LCOWSupported() && img.OS == "linux" {
... ...
@@ -123,7 +219,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
123 123
 }
124 124
 
125 125
 // Sets the Windows-specific fields of the OCI spec
126
-func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) {
126
+func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) error {
127 127
 	if len(s.Process.Cwd) == 0 {
128 128
 		// We default to C:\ to workaround the oddity of the case that the
129 129
 		// default directory for cmd running as LocalSystem (or
... ...
@@ -138,8 +234,14 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
138 138
 	s.Root.Readonly = false // Windows does not support a read-only root filesystem
139 139
 	if !isHyperV {
140 140
 		s.Root.Path = c.BaseFS // This is not set for Hyper-V containers
141
+		if !strings.HasSuffix(s.Root.Path, `\`) {
142
+			s.Root.Path = s.Root.Path + `\` // Ensure a correctly formatted volume GUID path \\?\Volume{GUID}\
143
+		}
141 144
 	}
142 145
 
146
+	// First boot optimization
147
+	s.Windows.IgnoreFlushesDuringBoot = !c.HasBeenStartedBefore
148
+
143 149
 	// In s.Windows.Resources
144 150
 	cpuShares := uint16(c.HostConfig.CPUShares)
145 151
 	cpuMaximum := uint16(c.HostConfig.CPUPercent) * 100
... ...
@@ -179,6 +281,54 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
179 179
 			Iops: &c.HostConfig.IOMaximumIOps,
180 180
 		},
181 181
 	}
182
+
183
+	// Read and add credentials from the security options if a credential spec has been provided.
184
+	if c.HostConfig.SecurityOpt != nil {
185
+		cs := ""
186
+		for _, sOpt := range c.HostConfig.SecurityOpt {
187
+			sOpt = strings.ToLower(sOpt)
188
+			if !strings.Contains(sOpt, "=") {
189
+				return fmt.Errorf("invalid security option: no equals sign in supplied value %s", sOpt)
190
+			}
191
+			var splitsOpt []string
192
+			splitsOpt = strings.SplitN(sOpt, "=", 2)
193
+			if len(splitsOpt) != 2 {
194
+				return fmt.Errorf("invalid security option: %s", sOpt)
195
+			}
196
+			if splitsOpt[0] != "credentialspec" {
197
+				return fmt.Errorf("security option not supported: %s", splitsOpt[0])
198
+			}
199
+
200
+			var (
201
+				match   bool
202
+				csValue string
203
+				err     error
204
+			)
205
+			if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match {
206
+				if csValue == "" {
207
+					return fmt.Errorf("no value supplied for file:// credential spec security option")
208
+				}
209
+				if cs, err = readCredentialSpecFile(c.ID, daemon.root, filepath.Clean(csValue)); err != nil {
210
+					return err
211
+				}
212
+			} else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match {
213
+				if csValue == "" {
214
+					return fmt.Errorf("no value supplied for registry:// credential spec security option")
215
+				}
216
+				if cs, err = readCredentialSpecRegistry(c.ID, csValue); err != nil {
217
+					return err
218
+				}
219
+			} else {
220
+				return fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
221
+			}
222
+		}
223
+		s.Windows.CredentialSpec = cs
224
+	}
225
+
226
+	// Assume we are not starting a container for a servicing operation
227
+	s.Windows.Servicing = false
228
+
229
+	return nil
182 230
 }
183 231
 
184 232
 // Sets the Linux-specific fields of the OCI spec
... ...
@@ -205,3 +355,52 @@ func escapeArgs(args []string) []string {
205 205
 func (daemon *Daemon) mergeUlimits(c *containertypes.HostConfig) {
206 206
 	return
207 207
 }
208
+
209
+// getCredentialSpec is a helper function to get the value of a credential spec supplied
210
+// on the CLI, stripping the prefix
211
+func getCredentialSpec(prefix, value string) (bool, string) {
212
+	if strings.HasPrefix(value, prefix) {
213
+		return true, strings.TrimPrefix(value, prefix)
214
+	}
215
+	return false, ""
216
+}
217
+
218
+// readCredentialSpecRegistry is a helper function to read a credential spec from
219
+// the registry. If not found, we return an empty string and warn in the log.
220
+// This allows for staging on machines which do not have the necessary components.
221
+func readCredentialSpecRegistry(id, name string) (string, error) {
222
+	var (
223
+		k   registry.Key
224
+		err error
225
+		val string
226
+	)
227
+	if k, err = registry.OpenKey(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE); err != nil {
228
+		return "", fmt.Errorf("failed handling spec %q for container %s - %s could not be opened", name, id, credentialSpecRegistryLocation)
229
+	}
230
+	if val, _, err = k.GetStringValue(name); err != nil {
231
+		if err == registry.ErrNotExist {
232
+			return "", fmt.Errorf("credential spec %q for container %s as it was not found", name, id)
233
+		}
234
+		return "", fmt.Errorf("error %v reading credential spec %q from registry for container %s", err, name, id)
235
+	}
236
+	return val, nil
237
+}
238
+
239
+// readCredentialSpecFile is a helper function to read a credential spec from
240
+// a file. If not found, we return an empty string and warn in the log.
241
+// This allows for staging on machines which do not have the necessary components.
242
+func readCredentialSpecFile(id, root, location string) (string, error) {
243
+	if filepath.IsAbs(location) {
244
+		return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute")
245
+	}
246
+	base := filepath.Join(root, credentialSpecFileLocation)
247
+	full := filepath.Join(base, location)
248
+	if !strings.HasPrefix(full, base) {
249
+		return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base)
250
+	}
251
+	bcontents, err := ioutil.ReadFile(full)
252
+	if err != nil {
253
+		return "", fmt.Errorf("credential spec '%s' for container %s as the file could not be read: %q", full, id, err)
254
+	}
255
+	return string(bcontents[:]), nil
256
+}
... ...
@@ -1,148 +1,14 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"io/ioutil"
6
-	"path/filepath"
7
-	"strings"
8
-
9 4
 	"github.com/Microsoft/opengcs/client"
10 5
 	"github.com/docker/docker/container"
11
-	"github.com/docker/docker/layer"
12 6
 	"github.com/docker/docker/libcontainerd"
13
-	"golang.org/x/sys/windows/registry"
14
-)
15
-
16
-const (
17
-	credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
18
-	credentialSpecFileLocation     = "CredentialSpecs"
19 7
 )
20 8
 
21 9
 func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) ([]libcontainerd.CreateOption, error) {
22 10
 	createOptions := []libcontainerd.CreateOption{}
23 11
 
24
-	// Are we going to run as a Hyper-V container?
25
-	hvOpts := &libcontainerd.HyperVIsolationOption{}
26
-	if container.HostConfig.Isolation.IsDefault() {
27
-		// Container is set to use the default, so take the default from the daemon configuration
28
-		hvOpts.IsHyperV = daemon.defaultIsolation.IsHyperV()
29
-	} else {
30
-		// Container is requesting an isolation mode. Honour it.
31
-		hvOpts.IsHyperV = container.HostConfig.Isolation.IsHyperV()
32
-	}
33
-
34
-	dnsSearch := daemon.getDNSSearchSettings(container)
35
-
36
-	// Generate the layer folder of the layer options
37
-	layerOpts := &libcontainerd.LayerOption{}
38
-	m, err := container.RWLayer.Metadata()
39
-	if err != nil {
40
-		return nil, fmt.Errorf("failed to get layer metadata - %s", err)
41
-	}
42
-	layerOpts.LayerFolderPath = m["dir"]
43
-
44
-	// Generate the layer paths of the layer options
45
-	img, err := daemon.stores[container.Platform].imageStore.Get(container.ImageID)
46
-	if err != nil {
47
-		return nil, fmt.Errorf("failed to graph.Get on ImageID %s - %s", container.ImageID, err)
48
-	}
49
-	// Get the layer path for each layer.
50
-	max := len(img.RootFS.DiffIDs)
51
-	for i := 1; i <= max; i++ {
52
-		img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i]
53
-		layerPath, err := layer.GetLayerPath(daemon.stores[container.Platform].layerStore, img.RootFS.ChainID())
54
-		if err != nil {
55
-			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)
56
-		}
57
-		// Reverse order, expecting parent most first
58
-		layerOpts.LayerPaths = append([]string{layerPath}, layerOpts.LayerPaths...)
59
-	}
60
-
61
-	// Get endpoints for the libnetwork allocated networks to the container
62
-	var epList []string
63
-	AllowUnqualifiedDNSQuery := false
64
-	gwHNSID := ""
65
-	if container.NetworkSettings != nil {
66
-		for n := range container.NetworkSettings.Networks {
67
-			sn, err := daemon.FindNetwork(n)
68
-			if err != nil {
69
-				continue
70
-			}
71
-
72
-			ep, err := container.GetEndpointInNetwork(sn)
73
-			if err != nil {
74
-				continue
75
-			}
76
-
77
-			data, err := ep.DriverInfo()
78
-			if err != nil {
79
-				continue
80
-			}
81
-
82
-			if data["GW_INFO"] != nil {
83
-				gwInfo := data["GW_INFO"].(map[string]interface{})
84
-				if gwInfo["hnsid"] != nil {
85
-					gwHNSID = gwInfo["hnsid"].(string)
86
-				}
87
-			}
88
-
89
-			if data["hnsid"] != nil {
90
-				epList = append(epList, data["hnsid"].(string))
91
-			}
92
-
93
-			if data["AllowUnqualifiedDNSQuery"] != nil {
94
-				AllowUnqualifiedDNSQuery = true
95
-			}
96
-		}
97
-	}
98
-
99
-	if gwHNSID != "" {
100
-		epList = append(epList, gwHNSID)
101
-	}
102
-
103
-	// Read and add credentials from the security options if a credential spec has been provided.
104
-	if container.HostConfig.SecurityOpt != nil {
105
-		for _, sOpt := range container.HostConfig.SecurityOpt {
106
-			sOpt = strings.ToLower(sOpt)
107
-			if !strings.Contains(sOpt, "=") {
108
-				return nil, fmt.Errorf("invalid security option: no equals sign in supplied value %s", sOpt)
109
-			}
110
-			var splitsOpt []string
111
-			splitsOpt = strings.SplitN(sOpt, "=", 2)
112
-			if len(splitsOpt) != 2 {
113
-				return nil, fmt.Errorf("invalid security option: %s", sOpt)
114
-			}
115
-			if splitsOpt[0] != "credentialspec" {
116
-				return nil, fmt.Errorf("security option not supported: %s", splitsOpt[0])
117
-			}
118
-
119
-			credentialsOpts := &libcontainerd.CredentialsOption{}
120
-			var (
121
-				match   bool
122
-				csValue string
123
-				err     error
124
-			)
125
-			if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match {
126
-				if csValue == "" {
127
-					return nil, fmt.Errorf("no value supplied for file:// credential spec security option")
128
-				}
129
-				if credentialsOpts.Credentials, err = readCredentialSpecFile(container.ID, daemon.root, filepath.Clean(csValue)); err != nil {
130
-					return nil, err
131
-				}
132
-			} else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match {
133
-				if csValue == "" {
134
-					return nil, fmt.Errorf("no value supplied for registry:// credential spec security option")
135
-				}
136
-				if credentialsOpts.Credentials, err = readCredentialSpecRegistry(container.ID, csValue); err != nil {
137
-					return nil, err
138
-				}
139
-			} else {
140
-				return nil, fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
141
-			}
142
-			createOptions = append(createOptions, credentialsOpts)
143
-		}
144
-	}
145
-
146 12
 	// LCOW options.
147 13
 	if container.Platform == "linux" {
148 14
 		config := &client.Config{}
... ...
@@ -173,73 +39,5 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain
173 173
 		createOptions = append(createOptions, lcowOpts)
174 174
 	}
175 175
 
176
-	// Now add the remaining options.
177
-	createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore})
178
-	createOptions = append(createOptions, hvOpts)
179
-	createOptions = append(createOptions, layerOpts)
180
-
181
-	var networkSharedContainerID string
182
-	if container.HostConfig.NetworkMode.IsContainer() {
183
-		networkSharedContainerID = container.NetworkSharedContainerID
184
-		for _, ep := range container.SharedEndpointList {
185
-			epList = append(epList, ep)
186
-		}
187
-	}
188
-
189
-	createOptions = append(createOptions, &libcontainerd.NetworkEndpointsOption{
190
-		Endpoints:                epList,
191
-		AllowUnqualifiedDNSQuery: AllowUnqualifiedDNSQuery,
192
-		DNSSearchList:            dnsSearch,
193
-		NetworkSharedContainerID: networkSharedContainerID,
194
-	})
195 176
 	return createOptions, nil
196 177
 }
197
-
198
-// getCredentialSpec is a helper function to get the value of a credential spec supplied
199
-// on the CLI, stripping the prefix
200
-func getCredentialSpec(prefix, value string) (bool, string) {
201
-	if strings.HasPrefix(value, prefix) {
202
-		return true, strings.TrimPrefix(value, prefix)
203
-	}
204
-	return false, ""
205
-}
206
-
207
-// readCredentialSpecRegistry is a helper function to read a credential spec from
208
-// the registry. If not found, we return an empty string and warn in the log.
209
-// This allows for staging on machines which do not have the necessary components.
210
-func readCredentialSpecRegistry(id, name string) (string, error) {
211
-	var (
212
-		k   registry.Key
213
-		err error
214
-		val string
215
-	)
216
-	if k, err = registry.OpenKey(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE); err != nil {
217
-		return "", fmt.Errorf("failed handling spec %q for container %s - %s could not be opened", name, id, credentialSpecRegistryLocation)
218
-	}
219
-	if val, _, err = k.GetStringValue(name); err != nil {
220
-		if err == registry.ErrNotExist {
221
-			return "", fmt.Errorf("credential spec %q for container %s as it was not found", name, id)
222
-		}
223
-		return "", fmt.Errorf("error %v reading credential spec %q from registry for container %s", err, name, id)
224
-	}
225
-	return val, nil
226
-}
227
-
228
-// readCredentialSpecFile is a helper function to read a credential spec from
229
-// a file. If not found, we return an empty string and warn in the log.
230
-// This allows for staging on machines which do not have the necessary components.
231
-func readCredentialSpecFile(id, root, location string) (string, error) {
232
-	if filepath.IsAbs(location) {
233
-		return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute")
234
-	}
235
-	base := filepath.Join(root, credentialSpecFileLocation)
236
-	full := filepath.Join(base, location)
237
-	if !strings.HasPrefix(full, base) {
238
-		return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base)
239
-	}
240
-	bcontents, err := ioutil.ReadFile(full)
241
-	if err != nil {
242
-		return "", fmt.Errorf("credential spec '%s' for container %s as the file could not be read: %q", full, id, err)
243
-	}
244
-	return string(bcontents[:]), nil
245
-}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"io/ioutil"
9 9
 	"os"
10 10
 	"path/filepath"
11
+	"regexp"
11 12
 	"strings"
12 13
 	"syscall"
13 14
 	"time"
... ...
@@ -102,8 +103,11 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir
102 102
 	if b, err := json.Marshal(spec); err == nil {
103 103
 		logrus.Debugln("libcontainerd: client.Create() with spec", string(b))
104 104
 	}
105
-	osName := spec.Platform.OS
106
-	if osName == "windows" {
105
+
106
+	// spec.Linux must be nil for Windows containers, but spec.Windows will be filled in regardless of container platform.
107
+	// This is a temporary workaround due to LCOW requiring layer folder paths, which are stored under spec.Windows.
108
+	// TODO: @darrenstahlmsft fix this once the OCI spec is updated to support layer folder paths for LCOW
109
+	if spec.Linux == nil {
107 110
 		return clnt.createWindows(containerID, checkpoint, checkpointDir, spec, attachStdio, options...)
108 111
 	}
109 112
 	return clnt.createLinux(containerID, checkpoint, checkpointDir, spec, attachStdio, options...)
... ...
@@ -114,9 +118,10 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
114 114
 		SystemType: "Container",
115 115
 		Name:       containerID,
116 116
 		Owner:      defaultOwner,
117
-		IgnoreFlushesDuringBoot: false,
117
+		IgnoreFlushesDuringBoot: spec.Windows.IgnoreFlushesDuringBoot,
118 118
 		HostName:                spec.Hostname,
119 119
 		HvPartition:             false,
120
+		Servicing:               spec.Windows.Servicing,
120 121
 	}
121 122
 
122 123
 	if spec.Windows.Resources != nil {
... ...
@@ -155,49 +160,43 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
155 155
 		}
156 156
 	}
157 157
 
158
-	var layerOpt *LayerOption
159
-	for _, option := range options {
160
-		if s, ok := option.(*ServicingOption); ok {
161
-			configuration.Servicing = s.IsServicing
162
-			continue
163
-		}
164
-		if f, ok := option.(*FlushOption); ok {
165
-			configuration.IgnoreFlushesDuringBoot = f.IgnoreFlushesDuringBoot
166
-			continue
167
-		}
168
-		if h, ok := option.(*HyperVIsolationOption); ok {
169
-			configuration.HvPartition = h.IsHyperV
170
-			continue
171
-		}
172
-		if l, ok := option.(*LayerOption); ok {
173
-			layerOpt = l
174
-		}
175
-		if n, ok := option.(*NetworkEndpointsOption); ok {
176
-			configuration.EndpointList = n.Endpoints
177
-			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
178
-			if n.DNSSearchList != nil {
179
-				configuration.DNSSearchList = strings.Join(n.DNSSearchList, ",")
180
-			}
181
-			configuration.NetworkSharedContainerName = n.NetworkSharedContainerID
182
-			continue
183
-		}
184
-		if c, ok := option.(*CredentialsOption); ok {
185
-			configuration.Credentials = c.Credentials
186
-			continue
158
+	if spec.Windows.HyperV != nil {
159
+		configuration.HvPartition = true
160
+	}
161
+
162
+	if spec.Windows.Network != nil {
163
+		configuration.EndpointList = spec.Windows.Network.EndpointList
164
+		configuration.AllowUnqualifiedDNSQuery = spec.Windows.Network.AllowUnqualifiedDNSQuery
165
+		if spec.Windows.Network.DNSSearchList != nil {
166
+			configuration.DNSSearchList = strings.Join(spec.Windows.Network.DNSSearchList, ",")
187 167
 		}
168
+		configuration.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName
169
+	}
170
+
171
+	if cs, ok := spec.Windows.CredentialSpec.(string); ok {
172
+		configuration.Credentials = cs
188 173
 	}
189 174
 
190
-	// We must have a layer option with at least one path
191
-	if layerOpt == nil || layerOpt.LayerPaths == nil {
192
-		return fmt.Errorf("no layer option or paths were supplied to the runtime")
175
+	// We must have least two layers in the spec, the bottom one being a base image,
176
+	// the top one being the RW layer.
177
+	if spec.Windows.LayerFolders == nil || len(spec.Windows.LayerFolders) < 2 {
178
+		return fmt.Errorf("OCI spec is invalid - at least two LayerFolders must be supplied to the runtime")
193 179
 	}
194 180
 
181
+	// Strip off the top-most layer as that's passed in separately to HCS
182
+	configuration.LayerFolderPath = spec.Windows.LayerFolders[len(spec.Windows.LayerFolders)-1]
183
+	layerFolders := spec.Windows.LayerFolders[:len(spec.Windows.LayerFolders)-1]
184
+
195 185
 	if configuration.HvPartition {
196
-		// Find the upper-most utility VM image, since the utility VM does not
197
-		// use layering in RS1.
198
-		// TODO @swernli/jhowardmsft at some point post RS1 this may be re-locatable.
186
+		// We don't currently support setting the utility VM image explicitly.
187
+		// TODO @swernli/jhowardmsft circa RS3/4, this may be re-locatable.
188
+		if spec.Windows.HyperV.UtilityVMPath != "" {
189
+			return errors.New("runtime does not support an explicit utility VM path for Hyper-V containers")
190
+		}
191
+
192
+		// Find the upper-most utility VM image.
199 193
 		var uvmImagePath string
200
-		for _, path := range layerOpt.LayerPaths {
194
+		for _, path := range layerFolders {
201 195
 			fullPath := filepath.Join(path, "UtilityVM")
202 196
 			_, err := os.Stat(fullPath)
203 197
 			if err == nil {
... ...
@@ -212,13 +211,24 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
212 212
 			return errors.New("utility VM image could not be found")
213 213
 		}
214 214
 		configuration.HvRuntime = &hcsshim.HvRuntime{ImagePath: uvmImagePath}
215
+
216
+		if spec.Root.Path != "" {
217
+			return errors.New("OCI spec is invalid - Root.Path must be omitted for a Hyper-V container")
218
+		}
215 219
 	} else {
216
-		configuration.VolumePath = spec.Root.Path
220
+		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}\}\\$`
221
+		if _, err := regexp.MatchString(volumeGUIDRegex, spec.Root.Path); err != nil {
222
+			return fmt.Errorf(`OCI spec is invalid - Root.Path '%s' must be a volume GUID path in the format '\\?\Volume{GUID}\'`, spec.Root.Path)
223
+		}
224
+		// HCS API requires the trailing backslash to be removed
225
+		configuration.VolumePath = spec.Root.Path[:len(spec.Root.Path)-1]
217 226
 	}
218 227
 
219
-	configuration.LayerFolderPath = layerOpt.LayerFolderPath
228
+	if spec.Root.Readonly {
229
+		return errors.New(`OCI spec is invalid - Root.Readonly must not be set on Windows`)
230
+	}
220 231
 
221
-	for _, layerPath := range layerOpt.LayerPaths {
232
+	for _, layerPath := range layerFolders {
222 233
 		_, filename := filepath.Split(layerPath)
223 234
 		g, err := hcsshim.NameToGuid(filename)
224 235
 		if err != nil {
... ...
@@ -235,6 +245,9 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
235 235
 	var mps []hcsshim.MappedPipe
236 236
 	for _, mount := range spec.Mounts {
237 237
 		const pipePrefix = `\\.\pipe\`
238
+		if mount.Type != "" {
239
+			return fmt.Errorf("OCI spec is invalid - Mount.Type '%s' must not be set", mount.Type)
240
+		}
238 241
 		if strings.HasPrefix(mount.Destination, pipePrefix) {
239 242
 			mp := hcsshim.MappedPipe{
240 243
 				HostPath:          mount.Source,
... ...
@@ -278,6 +291,7 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
278 278
 			},
279 279
 			processes: make(map[string]*process),
280 280
 		},
281
+		isWindows:    true,
281 282
 		ociSpec:      spec,
282 283
 		hcsContainer: hcsContainer,
283 284
 	}
... ...
@@ -306,12 +320,8 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
306 306
 func (clnt *client) createLinux(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error {
307 307
 	logrus.Debugf("libcontainerd: createLinux(): containerId %s ", containerID)
308 308
 
309
-	var layerOpt *LayerOption
310 309
 	var lcowOpt *LCOWOption
311 310
 	for _, option := range options {
312
-		if layer, ok := option.(*LayerOption); ok {
313
-			layerOpt = layer
314
-		}
315 311
 		if lcow, ok := option.(*LCOWOption); ok {
316 312
 			lcowOpt = lcow
317 313
 		}
... ...
@@ -342,14 +352,20 @@ func (clnt *client) createLinux(containerID string, checkpoint string, checkpoin
342 342
 		}
343 343
 	}
344 344
 
345
-	// We must have a layer option with at least one path
346
-	if layerOpt == nil || layerOpt.LayerPaths == nil {
347
-		return fmt.Errorf("no layer option or paths were supplied to the runtime")
345
+	if spec.Windows == nil {
346
+		return fmt.Errorf("spec.Windows must not be nil for LCOW containers")
347
+	}
348
+
349
+	// We must have least one layer in the spec
350
+	if spec.Windows.LayerFolders == nil || len(spec.Windows.LayerFolders) == 0 {
351
+		return fmt.Errorf("OCI spec is invalid - at least one LayerFolders must be supplied to the runtime")
348 352
 	}
349 353
 
350
-	// LayerFolderPath (writeable layer) + Layers (Guid + path)
351
-	configuration.LayerFolderPath = layerOpt.LayerFolderPath
352
-	for _, layerPath := range layerOpt.LayerPaths {
354
+	// Strip off the top-most layer as that's passed in separately to HCS
355
+	configuration.LayerFolderPath = spec.Windows.LayerFolders[len(spec.Windows.LayerFolders)-1]
356
+	layerFolders := spec.Windows.LayerFolders[:len(spec.Windows.LayerFolders)-1]
357
+
358
+	for _, layerPath := range layerFolders {
353 359
 		_, filename := filepath.Split(layerPath)
354 360
 		g, err := hcsshim.NameToGuid(filename)
355 361
 		if err != nil {
... ...
@@ -361,16 +377,13 @@ func (clnt *client) createLinux(containerID string, checkpoint string, checkpoin
361 361
 		})
362 362
 	}
363 363
 
364
-	for _, option := range options {
365
-		if n, ok := option.(*NetworkEndpointsOption); ok {
366
-			configuration.EndpointList = n.Endpoints
367
-			configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery
368
-			if n.DNSSearchList != nil {
369
-				configuration.DNSSearchList = strings.Join(n.DNSSearchList, ",")
370
-			}
371
-			configuration.NetworkSharedContainerName = n.NetworkSharedContainerID
372
-			break
364
+	if spec.Windows.Network != nil {
365
+		configuration.EndpointList = spec.Windows.Network.EndpointList
366
+		configuration.AllowUnqualifiedDNSQuery = spec.Windows.Network.AllowUnqualifiedDNSQuery
367
+		if spec.Windows.Network.DNSSearchList != nil {
368
+			configuration.DNSSearchList = strings.Join(spec.Windows.Network.DNSSearchList, ",")
373 369
 		}
370
+		configuration.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName
374 371
 	}
375 372
 
376 373
 	hcsContainer, err := hcsshim.CreateContainer(containerID, configuration)
... ...
@@ -436,8 +449,10 @@ func (clnt *client) AddProcess(ctx context.Context, containerID, processFriendly
436 436
 	}
437 437
 	if procToAdd.Terminal {
438 438
 		createProcessParms.EmulateConsole = true
439
-		createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height)
440
-		createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width)
439
+		if procToAdd.ConsoleSize != nil {
440
+			createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height)
441
+			createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width)
442
+		}
441 443
 	}
442 444
 
443 445
 	// 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
450 450
 
451 451
 	// Configure the environment for the process
452 452
 	createProcessParms.Environment = setupEnvironmentVariables(procToAdd.Env)
453
-	if container.ociSpec.Platform.OS == "windows" {
453
+	if container.isWindows {
454 454
 		createProcessParms.CommandLine = strings.Join(procToAdd.Args, " ")
455 455
 	} else {
456 456
 		createProcessParms.CommandArgs = procToAdd.Args
... ...
@@ -614,13 +629,8 @@ func (clnt *client) Pause(containerID string) error {
614 614
 		return err
615 615
 	}
616 616
 
617
-	for _, option := range container.options {
618
-		if h, ok := option.(*HyperVIsolationOption); ok {
619
-			if !h.IsHyperV {
620
-				return errors.New("cannot pause Windows Server Containers")
621
-			}
622
-			break
623
-		}
617
+	if container.ociSpec.Windows.HyperV == nil {
618
+		return errors.New("cannot pause Windows Server Containers")
624 619
 	}
625 620
 
626 621
 	err = container.hcsContainer.Pause()
... ...
@@ -654,13 +664,9 @@ func (clnt *client) Resume(containerID string) error {
654 654
 	}
655 655
 
656 656
 	// This should never happen, since Windows Server Containers cannot be paused
657
-	for _, option := range container.options {
658
-		if h, ok := option.(*HyperVIsolationOption); ok {
659
-			if !h.IsHyperV {
660
-				return errors.New("cannot resume Windows Server Containers")
661
-			}
662
-			break
663
-		}
657
+
658
+	if container.ociSpec.Windows.HyperV == nil {
659
+		return errors.New("cannot resume Windows Server Containers")
664 660
 	}
665 661
 
666 662
 	err = container.hcsContainer.Resume()
... ...
@@ -25,6 +25,7 @@ type container struct {
25 25
 	// otherwise have access to the Spec
26 26
 	ociSpec specs.Spec
27 27
 
28
+	isWindows           bool
28 29
 	manualStopRequested bool
29 30
 	hcsContainer        hcsshim.Container
30 31
 }
... ...
@@ -43,13 +44,6 @@ func (ctr *container) newProcess(friendlyName string) *process {
43 43
 // Caller needs to lock container ID before calling this method.
44 44
 func (ctr *container) start(attachStdio StdioCallback) error {
45 45
 	var err error
46
-	isServicing := false
47
-
48
-	for _, option := range ctr.options {
49
-		if s, ok := option.(*ServicingOption); ok && s.IsServicing {
50
-			isServicing = true
51
-		}
52
-	}
53 46
 
54 47
 	// Start the container.  If this is a servicing container, this call will block
55 48
 	// until the container is done with the servicing execution.
... ...
@@ -69,27 +63,39 @@ func (ctr *container) start(attachStdio StdioCallback) error {
69 69
 	// docker can always grab the output through logs. We also tell HCS to always
70 70
 	// create stdin, even if it's not used - it will be closed shortly. Stderr
71 71
 	// is only created if it we're not -t.
72
+	var (
73
+		emulateConsole   bool
74
+		createStdErrPipe bool
75
+	)
76
+	if ctr.ociSpec.Process != nil {
77
+		emulateConsole = ctr.ociSpec.Process.Terminal
78
+		createStdErrPipe = !ctr.ociSpec.Process.Terminal && !ctr.ociSpec.Windows.Servicing
79
+	}
80
+
72 81
 	createProcessParms := &hcsshim.ProcessConfig{
73
-		EmulateConsole:   ctr.ociSpec.Process.Terminal,
82
+		EmulateConsole:   emulateConsole,
74 83
 		WorkingDirectory: ctr.ociSpec.Process.Cwd,
75
-		CreateStdInPipe:  !isServicing,
76
-		CreateStdOutPipe: !isServicing,
77
-		CreateStdErrPipe: !ctr.ociSpec.Process.Terminal && !isServicing,
84
+		CreateStdInPipe:  !ctr.ociSpec.Windows.Servicing,
85
+		CreateStdOutPipe: !ctr.ociSpec.Windows.Servicing,
86
+		CreateStdErrPipe: createStdErrPipe,
87
+	}
88
+
89
+	if ctr.ociSpec.Process != nil && ctr.ociSpec.Process.ConsoleSize != nil {
90
+		createProcessParms.ConsoleSize[0] = uint(ctr.ociSpec.Process.ConsoleSize.Height)
91
+		createProcessParms.ConsoleSize[1] = uint(ctr.ociSpec.Process.ConsoleSize.Width)
78 92
 	}
79
-	createProcessParms.ConsoleSize[0] = uint(ctr.ociSpec.Process.ConsoleSize.Height)
80
-	createProcessParms.ConsoleSize[1] = uint(ctr.ociSpec.Process.ConsoleSize.Width)
81 93
 
82 94
 	// Configure the environment for the process
83 95
 	createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env)
84
-	if ctr.ociSpec.Platform.OS == "windows" {
96
+	if ctr.isWindows {
85 97
 		createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ")
86 98
 	} else {
87 99
 		createProcessParms.CommandArgs = ctr.ociSpec.Process.Args
88 100
 	}
89 101
 	createProcessParms.User = ctr.ociSpec.Process.User.Username
90 102
 
91
-	// Linux containers requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM.
92
-	if ctr.ociSpec.Platform.OS == "linux" {
103
+	// LCOW requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM.
104
+	if !ctr.isWindows {
93 105
 		ociBuf, err := json.Marshal(ctr.ociSpec)
94 106
 		if err != nil {
95 107
 			return err
... ...
@@ -118,7 +124,7 @@ func (ctr *container) start(attachStdio StdioCallback) error {
118 118
 
119 119
 	// If this is a servicing container, wait on the process synchronously here and
120 120
 	// if it succeeds, wait for it cleanly shutdown and merge into the parent container.
121
-	if isServicing {
121
+	if ctr.ociSpec.Windows.Servicing {
122 122
 		exitCode := ctr.waitProcessExitCode(&ctr.process)
123 123
 
124 124
 		if exitCode != 0 {
... ...
@@ -244,7 +250,7 @@ func (ctr *container) waitExit(process *process, isFirstProcessToStart bool) err
244 244
 		si.State = StateExitProcess
245 245
 	} else {
246 246
 		// Pending updates is only applicable for WCOW
247
-		if ctr.ociSpec.Platform.OS == "windows" {
247
+		if ctr.isWindows {
248 248
 			updatePending, err := ctr.hcsContainer.HasPendingUpdates()
249 249
 			if err != nil {
250 250
 				logrus.Warnf("libcontainerd: HasPendingUpdates() failed (container may have been killed): %s", err)
... ...
@@ -31,49 +31,6 @@ type LCOWOption struct {
31 31
 	Config *opengcs.Config
32 32
 }
33 33
 
34
-// ServicingOption is a CreateOption with a no-op application that signifies
35
-// the container needs to be used for a Windows servicing operation.
36
-type ServicingOption struct {
37
-	IsServicing bool
38
-}
39
-
40
-// FlushOption is a CreateOption that signifies if the container should be
41
-// started with flushes ignored until boot has completed. This is an optimisation
42
-// for first boot of a container.
43
-type FlushOption struct {
44
-	IgnoreFlushesDuringBoot bool
45
-}
46
-
47
-// HyperVIsolationOption is a CreateOption that indicates whether the runtime
48
-// should start the container as a Hyper-V container.
49
-type HyperVIsolationOption struct {
50
-	IsHyperV bool
51
-}
52
-
53
-// LayerOption is a CreateOption that indicates to the runtime the layer folder
54
-// and layer paths for a container.
55
-type LayerOption struct {
56
-	// LayerFolderPath is the path to the current layer folder. Empty for Hyper-V containers.
57
-	LayerFolderPath string `json:",omitempty"`
58
-	// Layer paths of the parent layers
59
-	LayerPaths []string
60
-}
61
-
62
-// NetworkEndpointsOption is a CreateOption that provides the runtime list
63
-// of network endpoints to which a container should be attached during its creation.
64
-type NetworkEndpointsOption struct {
65
-	Endpoints                []string
66
-	AllowUnqualifiedDNSQuery bool
67
-	DNSSearchList            []string
68
-	NetworkSharedContainerID string
69
-}
70
-
71
-// CredentialsOption is a CreateOption that indicates the credentials from
72
-// a credential spec to be used to the runtime
73
-type CredentialsOption struct {
74
-	Credentials string
75
-}
76
-
77 34
 // Checkpoint holds the details of a checkpoint (not supported in windows)
78 35
 type Checkpoint struct {
79 36
 	Name string
... ...
@@ -15,36 +15,6 @@ func setupEnvironmentVariables(a []string) map[string]string {
15 15
 	return r
16 16
 }
17 17
 
18
-// Apply for a servicing option is a no-op.
19
-func (s *ServicingOption) Apply(interface{}) error {
20
-	return nil
21
-}
22
-
23
-// Apply for the flush option is a no-op.
24
-func (f *FlushOption) Apply(interface{}) error {
25
-	return nil
26
-}
27
-
28
-// Apply for the hypervisolation option is a no-op.
29
-func (h *HyperVIsolationOption) Apply(interface{}) error {
30
-	return nil
31
-}
32
-
33
-// Apply for the layer option is a no-op.
34
-func (h *LayerOption) Apply(interface{}) error {
35
-	return nil
36
-}
37
-
38
-// Apply for the network endpoints option is a no-op.
39
-func (s *NetworkEndpointsOption) Apply(interface{}) error {
40
-	return nil
41
-}
42
-
43
-// Apply for the credentials option is a no-op.
44
-func (s *CredentialsOption) Apply(interface{}) error {
45
-	return nil
46
-}
47
-
48 18
 // Apply for the LCOW option is a no-op.
49 19
 func (s *LCOWOption) Apply(interface{}) error {
50 20
 	return nil
... ...
@@ -51,6 +51,8 @@ func DefaultWindowsSpec() specs.Spec {
51 51
 	return specs.Spec{
52 52
 		Version: specs.Version,
53 53
 		Windows: &specs.Windows{},
54
+		Process: &specs.Process{},
55
+		Root:    &specs.Root{},
54 56
 	}
55 57
 }
56 58
 
... ...
@@ -68,6 +70,7 @@ func DefaultLinuxSpec() specs.Spec {
68 68
 	s := specs.Spec{
69 69
 		Version: specs.Version,
70 70
 		Process: &specs.Process{},
71
+		Root:    &specs.Root{},
71 72
 	}
72 73
 	s.Mounts = []specs.Mount{
73 74
 		{
... ...
@@ -113,11 +116,13 @@ func DefaultLinuxSpec() specs.Spec {
113 113
 			Options:     []string{"nosuid", "noexec", "nodev", "mode=1777"},
114 114
 		},
115 115
 	}
116
-	s.Process.Capabilities = &specs.LinuxCapabilities{
117
-		Bounding:    defaultCapabilities(),
118
-		Permitted:   defaultCapabilities(),
119
-		Inheritable: defaultCapabilities(),
120
-		Effective:   defaultCapabilities(),
116
+	s.Process = &specs.Process{
117
+		Capabilities: &specs.LinuxCapabilities{
118
+			Bounding:    defaultCapabilities(),
119
+			Permitted:   defaultCapabilities(),
120
+			Inheritable: defaultCapabilities(),
121
+			Effective:   defaultCapabilities(),
122
+		},
121 123
 	}
122 124
 
123 125
 	s.Linux = &specs.Linux{
... ...
@@ -207,6 +212,11 @@ func DefaultLinuxSpec() specs.Spec {
207 207
 		},
208 208
 	}
209 209
 
210
+	// For LCOW support, populate a blank Windows spec
211
+	if runtime.GOOS == "windows" {
212
+		s.Windows = &specs.Windows{}
213
+	}
214
+
210 215
 	// For LCOW support, don't mask /sys/firmware
211 216
 	if runtime.GOOS != "windows" {
212 217
 		s.Linux.MaskedPaths = append(s.Linux.MaskedPaths, "/sys/firmware")