github.com/rumpl/bof@v23.0.0-rc.2+incompatible/daemon/oci_windows.go (about) 1 package daemon // import "github.com/docker/docker/daemon" 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 containertypes "github.com/docker/docker/api/types/container" 11 "github.com/docker/docker/container" 12 "github.com/docker/docker/errdefs" 13 "github.com/docker/docker/oci" 14 "github.com/docker/docker/pkg/sysinfo" 15 "github.com/docker/docker/pkg/system" 16 specs "github.com/opencontainers/runtime-spec/specs-go" 17 "github.com/pkg/errors" 18 "github.com/sirupsen/logrus" 19 "golang.org/x/sys/windows/registry" 20 ) 21 22 const ( 23 credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs` 24 credentialSpecFileLocation = "CredentialSpecs" 25 ) 26 27 func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { 28 29 img, err := daemon.imageService.GetImage(string(c.ImageID), nil) 30 if err != nil { 31 return nil, err 32 } 33 if !system.IsOSSupported(img.OperatingSystem()) { 34 return nil, system.ErrNotSupportedOperatingSystem 35 } 36 37 s := oci.DefaultSpec() 38 39 linkedEnv, err := daemon.setupLinkedContainers(c) 40 if err != nil { 41 return nil, err 42 } 43 44 // Note, unlike Unix, we do NOT call into SetupWorkingDirectory as 45 // this is done in VMCompute. Further, we couldn't do it for Hyper-V 46 // containers anyway. 47 48 if err := daemon.setupSecretDir(c); err != nil { 49 return nil, err 50 } 51 52 if err := daemon.setupConfigDir(c); err != nil { 53 return nil, err 54 } 55 56 // In s.Mounts 57 mounts, err := daemon.setupMounts(c) 58 if err != nil { 59 return nil, err 60 } 61 62 var isHyperV bool 63 if c.HostConfig.Isolation.IsDefault() { 64 // Container using default isolation, so take the default from the daemon configuration 65 isHyperV = daemon.defaultIsolation.IsHyperV() 66 } else { 67 // Container may be requesting an explicit isolation mode. 68 isHyperV = c.HostConfig.Isolation.IsHyperV() 69 } 70 71 if isHyperV { 72 s.Windows.HyperV = &specs.WindowsHyperV{} 73 } 74 75 // If the container has not been started, and has configs or secrets 76 // secrets, create symlinks to each config and secret. If it has been 77 // started before, the symlinks should have already been created. Also, it 78 // is important to not mount a Hyper-V container that has been started 79 // before, to protect the host from the container; for example, from 80 // malicious mutation of NTFS data structures. 81 if !c.HasBeenStartedBefore && (len(c.SecretReferences) > 0 || len(c.ConfigReferences) > 0) { 82 // The container file system is mounted before this function is called, 83 // except for Hyper-V containers, so mount it here in that case. 84 if isHyperV { 85 if err := daemon.Mount(c); err != nil { 86 return nil, err 87 } 88 defer daemon.Unmount(c) 89 } 90 if err := c.CreateSecretSymlinks(); err != nil { 91 return nil, err 92 } 93 if err := c.CreateConfigSymlinks(); err != nil { 94 return nil, err 95 } 96 } 97 98 secretMounts, err := c.SecretMounts() 99 if err != nil { 100 return nil, err 101 } 102 if secretMounts != nil { 103 mounts = append(mounts, secretMounts...) 104 } 105 106 configMounts := c.ConfigMounts() 107 if configMounts != nil { 108 mounts = append(mounts, configMounts...) 109 } 110 111 for _, mount := range mounts { 112 m := specs.Mount{ 113 Source: mount.Source, 114 Destination: mount.Destination, 115 } 116 if !mount.Writable { 117 m.Options = append(m.Options, "ro") 118 } 119 s.Mounts = append(s.Mounts, m) 120 } 121 122 // In s.Process 123 s.Process.Cwd = c.Config.WorkingDir 124 s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv) 125 s.Process.Terminal = c.Config.Tty 126 127 if c.Config.Tty { 128 s.Process.ConsoleSize = &specs.Box{ 129 Height: c.HostConfig.ConsoleSize[0], 130 Width: c.HostConfig.ConsoleSize[1], 131 } 132 } 133 s.Process.User.Username = c.Config.User 134 s.Windows.LayerFolders, err = daemon.imageService.GetLayerFolders(img, c.RWLayer) 135 if err != nil { 136 return nil, errors.Wrapf(err, "container %s", c.ID) 137 } 138 139 dnsSearch := daemon.getDNSSearchSettings(c) 140 141 // Get endpoints for the libnetwork allocated networks to the container 142 var epList []string 143 AllowUnqualifiedDNSQuery := false 144 gwHNSID := "" 145 if c.NetworkSettings != nil { 146 for n := range c.NetworkSettings.Networks { 147 sn, err := daemon.FindNetwork(n) 148 if err != nil { 149 continue 150 } 151 152 ep, err := getEndpointInNetwork(c.Name, sn) 153 if err != nil { 154 continue 155 } 156 157 data, err := ep.DriverInfo() 158 if err != nil { 159 continue 160 } 161 162 if data["GW_INFO"] != nil { 163 gwInfo := data["GW_INFO"].(map[string]interface{}) 164 if gwInfo["hnsid"] != nil { 165 gwHNSID = gwInfo["hnsid"].(string) 166 } 167 } 168 169 if data["hnsid"] != nil { 170 epList = append(epList, data["hnsid"].(string)) 171 } 172 173 if data["AllowUnqualifiedDNSQuery"] != nil { 174 AllowUnqualifiedDNSQuery = true 175 } 176 } 177 } 178 179 var networkSharedContainerID string 180 if c.HostConfig.NetworkMode.IsContainer() { 181 networkSharedContainerID = c.NetworkSharedContainerID 182 for _, ep := range c.SharedEndpointList { 183 epList = append(epList, ep) 184 } 185 } 186 187 if gwHNSID != "" { 188 epList = append(epList, gwHNSID) 189 } 190 191 s.Windows.Network = &specs.WindowsNetwork{ 192 AllowUnqualifiedDNSQuery: AllowUnqualifiedDNSQuery, 193 DNSSearchList: dnsSearch, 194 EndpointList: epList, 195 NetworkSharedContainerName: networkSharedContainerID, 196 } 197 198 if err := daemon.createSpecWindowsFields(c, &s, isHyperV); err != nil { 199 return nil, err 200 } 201 202 if logrus.IsLevelEnabled(logrus.DebugLevel) { 203 if b, err := json.Marshal(&s); err == nil { 204 logrus.Debugf("Generated spec: %s", string(b)) 205 } 206 } 207 208 return &s, nil 209 } 210 211 // Sets the Windows-specific fields of the OCI spec 212 func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) error { 213 214 s.Hostname = c.FullHostname() 215 216 if len(s.Process.Cwd) == 0 { 217 // We default to C:\ to workaround the oddity of the case that the 218 // default directory for cmd running as LocalSystem (or 219 // ContainerAdministrator) is c:\windows\system32. Hence docker run 220 // <image> cmd will by default end in c:\windows\system32, rather 221 // than 'root' (/) on Linux. The oddity is that if you have a dockerfile 222 // which has no WORKDIR and has a COPY file ., . will be interpreted 223 // as c:\. Hence, setting it to default of c:\ makes for consistency. 224 s.Process.Cwd = `C:\` 225 } 226 227 if c.Config.ArgsEscaped { 228 s.Process.CommandLine = c.Path 229 if len(c.Args) > 0 { 230 s.Process.CommandLine += " " + system.EscapeArgs(c.Args) 231 } 232 } else { 233 s.Process.Args = append([]string{c.Path}, c.Args...) 234 } 235 s.Root.Readonly = false // Windows does not support a read-only root filesystem 236 if !isHyperV { 237 if c.BaseFS == nil { 238 return errors.New("createSpecWindowsFields: BaseFS of container " + c.ID + " is unexpectedly nil") 239 } 240 241 s.Root.Path = c.BaseFS.Path() // This is not set for Hyper-V containers 242 if !strings.HasSuffix(s.Root.Path, `\`) { 243 s.Root.Path = s.Root.Path + `\` // Ensure a correctly formatted volume GUID path \\?\Volume{GUID}\ 244 } 245 } 246 247 // First boot optimization 248 s.Windows.IgnoreFlushesDuringBoot = !c.HasBeenStartedBefore 249 250 setResourcesInSpec(c, s, isHyperV) 251 252 // Read and add credentials from the security options if a credential spec has been provided. 253 if err := daemon.setWindowsCredentialSpec(c, s); err != nil { 254 return err 255 } 256 257 devices, err := setupWindowsDevices(c.HostConfig.Devices) 258 if err != nil { 259 return err 260 } 261 262 s.Windows.Devices = append(s.Windows.Devices, devices...) 263 264 return nil 265 } 266 267 var errInvalidCredentialSpecSecOpt = errdefs.InvalidParameter(fmt.Errorf("invalid credential spec security option - value must be prefixed by 'file://', 'registry://', or 'raw://' followed by a non-empty value")) 268 269 // setWindowsCredentialSpec sets the spec's `Windows.CredentialSpec` 270 // field if relevant 271 func (daemon *Daemon) setWindowsCredentialSpec(c *container.Container, s *specs.Spec) error { 272 if c.HostConfig == nil || c.HostConfig.SecurityOpt == nil { 273 return nil 274 } 275 276 // TODO (jrouge/wk8): if provided with several security options, we silently ignore 277 // all but the last one (provided they're all valid, otherwise we do return an error); 278 // this doesn't seem like a great idea? 279 credentialSpec := "" 280 281 for _, secOpt := range c.HostConfig.SecurityOpt { 282 optSplits := strings.SplitN(secOpt, "=", 2) 283 if len(optSplits) != 2 { 284 return errdefs.InvalidParameter(fmt.Errorf("invalid security option: no equals sign in supplied value %s", secOpt)) 285 } 286 if !strings.EqualFold(optSplits[0], "credentialspec") { 287 return errdefs.InvalidParameter(fmt.Errorf("security option not supported: %s", optSplits[0])) 288 } 289 290 credSpecSplits := strings.SplitN(optSplits[1], "://", 2) 291 if len(credSpecSplits) != 2 || credSpecSplits[1] == "" { 292 return errInvalidCredentialSpecSecOpt 293 } 294 value := credSpecSplits[1] 295 296 var err error 297 switch strings.ToLower(credSpecSplits[0]) { 298 case "file": 299 if credentialSpec, err = readCredentialSpecFile(c.ID, daemon.root, filepath.Clean(value)); err != nil { 300 return errdefs.InvalidParameter(err) 301 } 302 case "registry": 303 if credentialSpec, err = readCredentialSpecRegistry(c.ID, value); err != nil { 304 return errdefs.InvalidParameter(err) 305 } 306 case "config": 307 // if the container does not have a DependencyStore, then it 308 // isn't swarmkit managed. In order to avoid creating any 309 // impression that `config://` is a valid API, return the same 310 // error as if you'd passed any other random word. 311 if c.DependencyStore == nil { 312 return errInvalidCredentialSpecSecOpt 313 } 314 315 csConfig, err := c.DependencyStore.Configs().Get(value) 316 if err != nil { 317 return errdefs.System(errors.Wrap(err, "error getting value from config store")) 318 } 319 // stuff the resulting secret data into a string to use as the 320 // CredentialSpec 321 credentialSpec = string(csConfig.Spec.Data) 322 case "raw": 323 credentialSpec = value 324 default: 325 return errInvalidCredentialSpecSecOpt 326 } 327 } 328 329 if credentialSpec != "" { 330 if s.Windows == nil { 331 s.Windows = &specs.Windows{} 332 } 333 s.Windows.CredentialSpec = credentialSpec 334 } 335 336 return nil 337 } 338 339 func setResourcesInSpec(c *container.Container, s *specs.Spec, isHyperV bool) { 340 // In s.Windows.Resources 341 cpuShares := uint16(c.HostConfig.CPUShares) 342 cpuMaximum := uint16(c.HostConfig.CPUPercent) * 100 343 cpuCount := uint64(c.HostConfig.CPUCount) 344 if c.HostConfig.NanoCPUs > 0 { 345 if isHyperV { 346 cpuCount = uint64(c.HostConfig.NanoCPUs / 1e9) 347 leftoverNanoCPUs := c.HostConfig.NanoCPUs % 1e9 348 if leftoverNanoCPUs != 0 { 349 cpuCount++ 350 cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(cpuCount) / (1e9 / 10000)) 351 if cpuMaximum < 1 { 352 // The requested NanoCPUs is so small that we rounded to 0, use 1 instead 353 cpuMaximum = 1 354 } 355 } 356 } else { 357 cpuMaximum = uint16(c.HostConfig.NanoCPUs / int64(sysinfo.NumCPU()) / (1e9 / 10000)) 358 if cpuMaximum < 1 { 359 // The requested NanoCPUs is so small that we rounded to 0, use 1 instead 360 cpuMaximum = 1 361 } 362 } 363 } 364 365 if cpuMaximum != 0 || cpuShares != 0 || cpuCount != 0 { 366 if s.Windows.Resources == nil { 367 s.Windows.Resources = &specs.WindowsResources{} 368 } 369 s.Windows.Resources.CPU = &specs.WindowsCPUResources{ 370 Maximum: &cpuMaximum, 371 Shares: &cpuShares, 372 Count: &cpuCount, 373 } 374 } 375 376 memoryLimit := uint64(c.HostConfig.Memory) 377 if memoryLimit != 0 { 378 if s.Windows.Resources == nil { 379 s.Windows.Resources = &specs.WindowsResources{} 380 } 381 s.Windows.Resources.Memory = &specs.WindowsMemoryResources{ 382 Limit: &memoryLimit, 383 } 384 } 385 386 if c.HostConfig.IOMaximumBandwidth != 0 || c.HostConfig.IOMaximumIOps != 0 { 387 if s.Windows.Resources == nil { 388 s.Windows.Resources = &specs.WindowsResources{} 389 } 390 s.Windows.Resources.Storage = &specs.WindowsStorageResources{ 391 Bps: &c.HostConfig.IOMaximumBandwidth, 392 Iops: &c.HostConfig.IOMaximumIOps, 393 } 394 } 395 } 396 397 // mergeUlimits merge the Ulimits from HostConfig with daemon defaults, and update HostConfig 398 // It will do nothing on non-Linux platform 399 func (daemon *Daemon) mergeUlimits(c *containertypes.HostConfig) { 400 return 401 } 402 403 // registryKey is an interface wrapper around `registry.Key`, 404 // listing only the methods we care about here. 405 // It's mainly useful to easily allow mocking the registry in tests. 406 type registryKey interface { 407 GetStringValue(name string) (val string, valtype uint32, err error) 408 Close() error 409 } 410 411 var registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) { 412 return registry.OpenKey(baseKey, path, access) 413 } 414 415 // readCredentialSpecRegistry is a helper function to read a credential spec from 416 // the registry. If not found, we return an empty string and warn in the log. 417 // This allows for staging on machines which do not have the necessary components. 418 func readCredentialSpecRegistry(id, name string) (string, error) { 419 key, err := registryOpenKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE) 420 if err != nil { 421 return "", errors.Wrapf(err, "failed handling spec %q for container %s - registry key %s could not be opened", name, id, credentialSpecRegistryLocation) 422 } 423 defer key.Close() 424 425 value, _, err := key.GetStringValue(name) 426 if err != nil { 427 if err == registry.ErrNotExist { 428 return "", fmt.Errorf("registry credential spec %q for container %s was not found", name, id) 429 } 430 return "", errors.Wrapf(err, "error reading credential spec %q from registry for container %s", name, id) 431 } 432 433 return value, nil 434 } 435 436 // readCredentialSpecFile is a helper function to read a credential spec from 437 // a file. If not found, we return an empty string and warn in the log. 438 // This allows for staging on machines which do not have the necessary components. 439 func readCredentialSpecFile(id, root, location string) (string, error) { 440 if filepath.IsAbs(location) { 441 return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute") 442 } 443 base := filepath.Join(root, credentialSpecFileLocation) 444 full := filepath.Join(base, location) 445 if !strings.HasPrefix(full, base) { 446 return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base) 447 } 448 bcontents, err := os.ReadFile(full) 449 if err != nil { 450 return "", errors.Wrapf(err, "credential spec for container %s could not be read from file %q", id, full) 451 } 452 return string(bcontents[:]), nil 453 } 454 455 func setupWindowsDevices(devices []containertypes.DeviceMapping) (specDevices []specs.WindowsDevice, err error) { 456 if len(devices) == 0 { 457 return 458 } 459 460 for _, deviceMapping := range devices { 461 devicePath := deviceMapping.PathOnHost 462 if strings.HasPrefix(devicePath, "class/") { 463 devicePath = strings.Replace(devicePath, "class/", "class://", 1) 464 } 465 466 srcParts := strings.SplitN(devicePath, "://", 2) 467 if len(srcParts) != 2 { 468 return nil, errors.Errorf("invalid device assignment path: '%s', must be 'class/ID' or 'IDType://ID'", deviceMapping.PathOnHost) 469 } 470 if srcParts[0] == "" { 471 return nil, errors.Errorf("invalid device assignment path: '%s', IDType cannot be empty", deviceMapping.PathOnHost) 472 } 473 wd := specs.WindowsDevice{ 474 ID: srcParts[1], 475 IDType: srcParts[0], 476 } 477 specDevices = append(specDevices, wd) 478 } 479 480 return 481 }