github.com/nektos/act@v0.2.63-0.20240520024548-8acde99bfa9c/pkg/container/docker_cli.go (about) 1 //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) 2 3 // This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts.go 4 // appended with license information. 5 // 6 // docker/cli is licensed under the Apache License, Version 2.0. 7 // See DOCKER_LICENSE for the full license text. 8 // 9 10 //nolint:unparam,errcheck,depguard,deadcode,unused 11 package container 12 13 import ( 14 "bytes" 15 "encoding/json" 16 "fmt" 17 "os" 18 "path" 19 "path/filepath" 20 "reflect" 21 "regexp" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/docker/cli/cli/compose/loader" 27 "github.com/docker/cli/opts" 28 "github.com/docker/docker/api/types/container" 29 mounttypes "github.com/docker/docker/api/types/mount" 30 networktypes "github.com/docker/docker/api/types/network" 31 "github.com/docker/docker/api/types/strslice" 32 "github.com/docker/docker/api/types/versions" 33 "github.com/docker/docker/errdefs" 34 "github.com/docker/go-connections/nat" 35 "github.com/pkg/errors" 36 "github.com/sirupsen/logrus" 37 "github.com/spf13/pflag" 38 ) 39 40 var ( 41 deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`) 42 ) 43 44 // containerOptions is a data object with all the options for creating a container 45 type containerOptions struct { 46 attach opts.ListOpts 47 volumes opts.ListOpts 48 tmpfs opts.ListOpts 49 mounts opts.MountOpt 50 blkioWeightDevice opts.WeightdeviceOpt 51 deviceReadBps opts.ThrottledeviceOpt 52 deviceWriteBps opts.ThrottledeviceOpt 53 links opts.ListOpts 54 aliases opts.ListOpts 55 linkLocalIPs opts.ListOpts 56 deviceReadIOps opts.ThrottledeviceOpt 57 deviceWriteIOps opts.ThrottledeviceOpt 58 env opts.ListOpts 59 labels opts.ListOpts 60 deviceCgroupRules opts.ListOpts 61 devices opts.ListOpts 62 gpus opts.GpuOpts 63 ulimits *opts.UlimitOpt 64 sysctls *opts.MapOpts 65 publish opts.ListOpts 66 expose opts.ListOpts 67 dns opts.ListOpts 68 dnsSearch opts.ListOpts 69 dnsOptions opts.ListOpts 70 extraHosts opts.ListOpts 71 volumesFrom opts.ListOpts 72 envFile opts.ListOpts 73 capAdd opts.ListOpts 74 capDrop opts.ListOpts 75 groupAdd opts.ListOpts 76 securityOpt opts.ListOpts 77 storageOpt opts.ListOpts 78 labelsFile opts.ListOpts 79 loggingOpts opts.ListOpts 80 privileged bool 81 pidMode string 82 utsMode string 83 usernsMode string 84 cgroupnsMode string 85 publishAll bool 86 stdin bool 87 tty bool 88 oomKillDisable bool 89 oomScoreAdj int 90 containerIDFile string 91 entrypoint string 92 hostname string 93 domainname string 94 memory opts.MemBytes 95 memoryReservation opts.MemBytes 96 memorySwap opts.MemSwapBytes 97 kernelMemory opts.MemBytes 98 user string 99 workingDir string 100 cpuCount int64 101 cpuShares int64 102 cpuPercent int64 103 cpuPeriod int64 104 cpuRealtimePeriod int64 105 cpuRealtimeRuntime int64 106 cpuQuota int64 107 cpus opts.NanoCPUs 108 cpusetCpus string 109 cpusetMems string 110 blkioWeight uint16 111 ioMaxBandwidth opts.MemBytes 112 ioMaxIOps uint64 113 swappiness int64 114 netMode opts.NetworkOpt 115 macAddress string 116 ipv4Address string 117 ipv6Address string 118 ipcMode string 119 pidsLimit int64 120 restartPolicy string 121 readonlyRootfs bool 122 loggingDriver string 123 cgroupParent string 124 volumeDriver string 125 stopSignal string 126 stopTimeout int 127 isolation string 128 shmSize opts.MemBytes 129 noHealthcheck bool 130 healthCmd string 131 healthInterval time.Duration 132 healthTimeout time.Duration 133 healthStartPeriod time.Duration 134 healthRetries int 135 runtime string 136 autoRemove bool 137 init bool 138 139 Image string 140 Args []string 141 } 142 143 // addFlags adds all command line flags that will be used by parse to the FlagSet 144 func addFlags(flags *pflag.FlagSet) *containerOptions { 145 copts := &containerOptions{ 146 aliases: opts.NewListOpts(nil), 147 attach: opts.NewListOpts(validateAttach), 148 blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice), 149 capAdd: opts.NewListOpts(nil), 150 capDrop: opts.NewListOpts(nil), 151 dns: opts.NewListOpts(opts.ValidateIPAddress), 152 dnsOptions: opts.NewListOpts(nil), 153 dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), 154 deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule), 155 deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), 156 deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), 157 deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), 158 deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), 159 devices: opts.NewListOpts(nil), // devices can only be validated after we know the server OS 160 env: opts.NewListOpts(opts.ValidateEnv), 161 envFile: opts.NewListOpts(nil), 162 expose: opts.NewListOpts(nil), 163 extraHosts: opts.NewListOpts(opts.ValidateExtraHost), 164 groupAdd: opts.NewListOpts(nil), 165 labels: opts.NewListOpts(opts.ValidateLabel), 166 labelsFile: opts.NewListOpts(nil), 167 linkLocalIPs: opts.NewListOpts(nil), 168 links: opts.NewListOpts(opts.ValidateLink), 169 loggingOpts: opts.NewListOpts(nil), 170 publish: opts.NewListOpts(nil), 171 securityOpt: opts.NewListOpts(nil), 172 storageOpt: opts.NewListOpts(nil), 173 sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl), 174 tmpfs: opts.NewListOpts(nil), 175 ulimits: opts.NewUlimitOpt(nil), 176 volumes: opts.NewListOpts(nil), 177 volumesFrom: opts.NewListOpts(nil), 178 } 179 180 // General purpose flags 181 flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") 182 flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list") 183 flags.Var(&copts.devices, "device", "Add a host device to the container") 184 flags.Var(&copts.gpus, "gpus", "GPU devices to add to the container ('all' to pass all GPUs)") 185 flags.SetAnnotation("gpus", "version", []string{"1.40"}) 186 flags.VarP(&copts.env, "env", "e", "Set environment variables") 187 flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables") 188 flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image") 189 flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join") 190 flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name") 191 flags.StringVar(&copts.domainname, "domainname", "", "Container NIS domain name") 192 flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached") 193 flags.VarP(&copts.labels, "label", "l", "Set meta data on a container") 194 flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") 195 flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") 196 flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") 197 flags.StringVar(&copts.stopSignal, "stop-signal", "", "Signal to stop the container") 198 flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") 199 flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) 200 flags.Var(copts.sysctls, "sysctl", "Sysctl options") 201 flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") 202 flags.Var(copts.ulimits, "ulimit", "Ulimit options") 203 flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])") 204 flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container") 205 flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits") 206 207 // Security 208 flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities") 209 flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities") 210 flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") 211 flags.Var(&copts.securityOpt, "security-opt", "Security Options") 212 flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") 213 flags.StringVar(&copts.cgroupnsMode, "cgroupns", "", `Cgroup namespace to use (host|private) 214 'host': Run the container in the Docker host's cgroup namespace 215 'private': Run the container in its own private cgroup namespace 216 '': Use the cgroup namespace as configured by the 217 default-cgroupns-mode option on the daemon (default)`) 218 flags.SetAnnotation("cgroupns", "version", []string{"1.41"}) 219 220 // Network and port publishing flag 221 flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") 222 flags.Var(&copts.dns, "dns", "Set custom DNS servers") 223 // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. 224 // This is to be consistent with service create/update 225 flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options") 226 flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options") 227 flags.MarkHidden("dns-opt") 228 flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") 229 flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") 230 flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)") 231 flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") 232 flags.Var(&copts.links, "link", "Add link to another container") 233 flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") 234 flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)") 235 flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host") 236 flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports") 237 // We allow for both "--net" and "--network", although the latter is the recommended way. 238 flags.Var(&copts.netMode, "net", "Connect a container to a network") 239 flags.Var(&copts.netMode, "network", "Connect a container to a network") 240 flags.MarkHidden("net") 241 // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way. 242 flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container") 243 flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container") 244 flags.MarkHidden("net-alias") 245 246 // Logging and storage 247 flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container") 248 flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container") 249 flags.Var(&copts.loggingOpts, "log-opt", "Log driver options") 250 flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container") 251 flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") 252 flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") 253 flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") 254 flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container") 255 256 // Health-checking 257 flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") 258 flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)") 259 flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") 260 flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)") 261 flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)") 262 flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) 263 flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") 264 265 // Resource management 266 flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") 267 flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)") 268 flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file") 269 flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") 270 flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") 271 flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") 272 flags.SetAnnotation("cpu-count", "ostype", []string{"windows"}) 273 flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") 274 flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"}) 275 flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") 276 flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") 277 flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") 278 flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) 279 flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") 280 flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) 281 flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") 282 flags.Var(&copts.cpus, "cpus", "Number of CPUs") 283 flags.SetAnnotation("cpus", "version", []string{"1.25"}) 284 flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") 285 flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") 286 flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") 287 flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") 288 flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)") 289 flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) 290 flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") 291 flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) 292 flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") 293 flags.VarP(&copts.memory, "memory", "m", "Memory limit") 294 flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") 295 flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") 296 flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") 297 flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer") 298 flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)") 299 flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)") 300 301 // Low-level execution (cgroups, namespaces, ...) 302 flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") 303 flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use") 304 flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology") 305 flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use") 306 flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm") 307 flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") 308 flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") 309 310 flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") 311 flags.SetAnnotation("init", "version", []string{"1.25"}) 312 return copts 313 } 314 315 type containerConfig struct { 316 Config *container.Config 317 HostConfig *container.HostConfig 318 NetworkingConfig *networktypes.NetworkingConfig 319 } 320 321 // parse parses the args for the specified command and generates a Config, 322 // a HostConfig and returns them with the specified command. 323 // If the specified args are not valid, it will return an error. 324 // 325 //nolint:gocyclo 326 func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) { 327 var ( 328 attachStdin = copts.attach.Get("stdin") 329 attachStdout = copts.attach.Get("stdout") 330 attachStderr = copts.attach.Get("stderr") 331 ) 332 333 // Validate the input mac address 334 if copts.macAddress != "" { 335 if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { 336 return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) 337 } 338 } 339 if copts.stdin { 340 attachStdin = true 341 } 342 // If -a is not set, attach to stdout and stderr 343 if copts.attach.Len() == 0 { 344 attachStdout = true 345 attachStderr = true 346 } 347 348 var err error 349 350 swappiness := copts.swappiness 351 if swappiness != -1 && (swappiness < 0 || swappiness > 100) { 352 return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) 353 } 354 355 mounts := copts.mounts.Value() 356 if len(mounts) > 0 && copts.volumeDriver != "" { 357 logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.") 358 } 359 var binds []string 360 volumes := copts.volumes.GetMap() 361 // add any bind targets to the list of container volumes 362 for bind := range copts.volumes.GetMap() { 363 parsed, _ := loader.ParseVolume(bind) 364 365 if parsed.Source != "" { 366 toBind := bind 367 368 if parsed.Type == string(mounttypes.TypeBind) { 369 if arr := strings.SplitN(bind, ":", 2); len(arr) == 2 { 370 hostPart := arr[0] 371 if strings.HasPrefix(hostPart, "."+string(filepath.Separator)) || hostPart == "." { 372 if absHostPart, err := filepath.Abs(hostPart); err == nil { 373 hostPart = absHostPart 374 } 375 } 376 toBind = hostPart + ":" + arr[1] 377 } 378 } 379 380 // after creating the bind mount we want to delete it from the copts.volumes values because 381 // we do not want bind mounts being committed to image configs 382 binds = append(binds, toBind) 383 // We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if 384 // there are duplicates entries. 385 delete(volumes, bind) 386 } 387 } 388 389 // Can't evaluate options passed into --tmpfs until we actually mount 390 tmpfs := make(map[string]string) 391 for _, t := range copts.tmpfs.GetAll() { 392 if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { 393 tmpfs[arr[0]] = arr[1] 394 } else { 395 tmpfs[arr[0]] = "" 396 } 397 } 398 399 var ( 400 runCmd strslice.StrSlice 401 entrypoint strslice.StrSlice 402 ) 403 404 if len(copts.Args) > 0 { 405 runCmd = strslice.StrSlice(copts.Args) 406 } 407 408 if copts.entrypoint != "" { 409 entrypoint = strslice.StrSlice{copts.entrypoint} 410 } else if flags.Changed("entrypoint") { 411 // if `--entrypoint=` is parsed then Entrypoint is reset 412 entrypoint = []string{""} 413 } 414 415 publishOpts := copts.publish.GetAll() 416 var ( 417 ports map[nat.Port]struct{} 418 portBindings map[nat.Port][]nat.PortBinding 419 convertedOpts []string 420 ) 421 422 convertedOpts, err = convertToStandardNotation(publishOpts) 423 if err != nil { 424 return nil, err 425 } 426 427 ports, portBindings, err = nat.ParsePortSpecs(convertedOpts) 428 if err != nil { 429 return nil, err 430 } 431 432 // Merge in exposed ports to the map of published ports 433 for _, e := range copts.expose.GetAll() { 434 if strings.Contains(e, ":") { 435 return nil, errors.Errorf("invalid port format for --expose: %s", e) 436 } 437 // support two formats for expose, original format <portnum>/[<proto>] 438 // or <startport-endport>/[<proto>] 439 proto, port := nat.SplitProtoPort(e) 440 // parse the start and end port and create a sequence of ports to expose 441 // if expose a port, the start and end port are the same 442 start, end, err := nat.ParsePortRange(port) 443 if err != nil { 444 return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) 445 } 446 for i := start; i <= end; i++ { 447 p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) 448 if err != nil { 449 return nil, err 450 } 451 if _, exists := ports[p]; !exists { 452 ports[p] = struct{}{} 453 } 454 } 455 } 456 457 // validate and parse device mappings. Note we do late validation of the 458 // device path (as opposed to during flag parsing), as at the time we are 459 // parsing flags, we haven't yet sent a _ping to the daemon to determine 460 // what operating system it is. 461 deviceMappings := []container.DeviceMapping{} 462 for _, device := range copts.devices.GetAll() { 463 var ( 464 validated string 465 deviceMapping container.DeviceMapping 466 err error 467 ) 468 validated, err = validateDevice(device, serverOS) 469 if err != nil { 470 return nil, err 471 } 472 deviceMapping, err = parseDevice(validated, serverOS) 473 if err != nil { 474 return nil, err 475 } 476 deviceMappings = append(deviceMappings, deviceMapping) 477 } 478 479 // collect all the environment variables for the container 480 envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetAll(), copts.env.GetAll()) 481 if err != nil { 482 return nil, err 483 } 484 485 // collect all the labels for the container 486 labels, err := opts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll()) 487 if err != nil { 488 return nil, err 489 } 490 491 pidMode := container.PidMode(copts.pidMode) 492 if !pidMode.Valid() { 493 return nil, errors.Errorf("--pid: invalid PID mode") 494 } 495 496 utsMode := container.UTSMode(copts.utsMode) 497 if !utsMode.Valid() { 498 return nil, errors.Errorf("--uts: invalid UTS mode") 499 } 500 501 usernsMode := container.UsernsMode(copts.usernsMode) 502 if !usernsMode.Valid() { 503 return nil, errors.Errorf("--userns: invalid USER mode") 504 } 505 506 cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode) 507 if !cgroupnsMode.Valid() { 508 return nil, errors.Errorf("--cgroupns: invalid CGROUP mode") 509 } 510 511 restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy) 512 if err != nil { 513 return nil, err 514 } 515 516 loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll()) 517 if err != nil { 518 return nil, err 519 } 520 521 securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll()) 522 if err != nil { 523 return nil, err 524 } 525 526 securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(securityOpts) 527 528 storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll()) 529 if err != nil { 530 return nil, err 531 } 532 533 // Healthcheck 534 var healthConfig *container.HealthConfig 535 haveHealthSettings := copts.healthCmd != "" || 536 copts.healthInterval != 0 || 537 copts.healthTimeout != 0 || 538 copts.healthStartPeriod != 0 || 539 copts.healthRetries != 0 540 if copts.noHealthcheck { 541 if haveHealthSettings { 542 return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") 543 } 544 test := strslice.StrSlice{"NONE"} 545 healthConfig = &container.HealthConfig{Test: test} 546 } else if haveHealthSettings { 547 var probe strslice.StrSlice 548 if copts.healthCmd != "" { 549 args := []string{"CMD-SHELL", copts.healthCmd} 550 probe = strslice.StrSlice(args) 551 } 552 if copts.healthInterval < 0 { 553 return nil, errors.Errorf("--health-interval cannot be negative") 554 } 555 if copts.healthTimeout < 0 { 556 return nil, errors.Errorf("--health-timeout cannot be negative") 557 } 558 if copts.healthRetries < 0 { 559 return nil, errors.Errorf("--health-retries cannot be negative") 560 } 561 if copts.healthStartPeriod < 0 { 562 return nil, fmt.Errorf("--health-start-period cannot be negative") 563 } 564 565 healthConfig = &container.HealthConfig{ 566 Test: probe, 567 Interval: copts.healthInterval, 568 Timeout: copts.healthTimeout, 569 StartPeriod: copts.healthStartPeriod, 570 Retries: copts.healthRetries, 571 } 572 } 573 574 resources := container.Resources{ 575 CgroupParent: copts.cgroupParent, 576 Memory: copts.memory.Value(), 577 MemoryReservation: copts.memoryReservation.Value(), 578 MemorySwap: copts.memorySwap.Value(), 579 MemorySwappiness: &copts.swappiness, 580 KernelMemory: copts.kernelMemory.Value(), 581 OomKillDisable: &copts.oomKillDisable, 582 NanoCPUs: copts.cpus.Value(), 583 CPUCount: copts.cpuCount, 584 CPUPercent: copts.cpuPercent, 585 CPUShares: copts.cpuShares, 586 CPUPeriod: copts.cpuPeriod, 587 CpusetCpus: copts.cpusetCpus, 588 CpusetMems: copts.cpusetMems, 589 CPUQuota: copts.cpuQuota, 590 CPURealtimePeriod: copts.cpuRealtimePeriod, 591 CPURealtimeRuntime: copts.cpuRealtimeRuntime, 592 PidsLimit: &copts.pidsLimit, 593 BlkioWeight: copts.blkioWeight, 594 BlkioWeightDevice: copts.blkioWeightDevice.GetList(), 595 BlkioDeviceReadBps: copts.deviceReadBps.GetList(), 596 BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(), 597 BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), 598 BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), 599 IOMaximumIOps: copts.ioMaxIOps, 600 IOMaximumBandwidth: uint64(copts.ioMaxBandwidth), 601 Ulimits: copts.ulimits.GetList(), 602 DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), 603 Devices: deviceMappings, 604 DeviceRequests: copts.gpus.Value(), 605 } 606 607 config := &container.Config{ 608 Hostname: copts.hostname, 609 Domainname: copts.domainname, 610 ExposedPorts: ports, 611 User: copts.user, 612 Tty: copts.tty, 613 // TODO: deprecated, it comes from -n, --networking 614 // it's still needed internally to set the network to disabled 615 // if e.g. bridge is none in daemon opts, and in inspect 616 NetworkDisabled: false, 617 OpenStdin: copts.stdin, 618 AttachStdin: attachStdin, 619 AttachStdout: attachStdout, 620 AttachStderr: attachStderr, 621 Env: envVariables, 622 Cmd: runCmd, 623 Image: copts.Image, 624 Volumes: volumes, 625 MacAddress: copts.macAddress, 626 Entrypoint: entrypoint, 627 WorkingDir: copts.workingDir, 628 Labels: opts.ConvertKVStringsToMap(labels), 629 StopSignal: copts.stopSignal, 630 Healthcheck: healthConfig, 631 } 632 if flags.Changed("stop-timeout") { 633 config.StopTimeout = &copts.stopTimeout 634 } 635 636 hostConfig := &container.HostConfig{ 637 Binds: binds, 638 ContainerIDFile: copts.containerIDFile, 639 OomScoreAdj: copts.oomScoreAdj, 640 AutoRemove: copts.autoRemove, 641 Privileged: copts.privileged, 642 PortBindings: portBindings, 643 Links: copts.links.GetAll(), 644 PublishAllPorts: copts.publishAll, 645 // Make sure the dns fields are never nil. 646 // New containers don't ever have those fields nil, 647 // but pre created containers can still have those nil values. 648 // See https://github.com/docker/docker/pull/17779 649 // for a more detailed explanation on why we don't want that. 650 DNS: copts.dns.GetAllOrEmpty(), 651 DNSSearch: copts.dnsSearch.GetAllOrEmpty(), 652 DNSOptions: copts.dnsOptions.GetAllOrEmpty(), 653 ExtraHosts: copts.extraHosts.GetAll(), 654 VolumesFrom: copts.volumesFrom.GetAll(), 655 IpcMode: container.IpcMode(copts.ipcMode), 656 NetworkMode: container.NetworkMode(copts.netMode.NetworkMode()), 657 PidMode: pidMode, 658 UTSMode: utsMode, 659 UsernsMode: usernsMode, 660 CgroupnsMode: cgroupnsMode, 661 CapAdd: strslice.StrSlice(copts.capAdd.GetAll()), 662 CapDrop: strslice.StrSlice(copts.capDrop.GetAll()), 663 GroupAdd: copts.groupAdd.GetAll(), 664 RestartPolicy: restartPolicy, 665 SecurityOpt: securityOpts, 666 StorageOpt: storageOpts, 667 ReadonlyRootfs: copts.readonlyRootfs, 668 LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts}, 669 VolumeDriver: copts.volumeDriver, 670 Isolation: container.Isolation(copts.isolation), 671 ShmSize: copts.shmSize.Value(), 672 Resources: resources, 673 Tmpfs: tmpfs, 674 Sysctls: copts.sysctls.GetAll(), 675 Runtime: copts.runtime, 676 Mounts: mounts, 677 MaskedPaths: maskedPaths, 678 ReadonlyPaths: readonlyPaths, 679 } 680 681 if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { 682 return nil, errors.Errorf("Conflicting options: --restart and --rm") 683 } 684 685 // only set this value if the user provided the flag, else it should default to nil 686 if flags.Changed("init") { 687 hostConfig.Init = &copts.init 688 } 689 690 // When allocating stdin in attached mode, close stdin at client disconnect 691 if config.OpenStdin && config.AttachStdin { 692 config.StdinOnce = true 693 } 694 695 networkingConfig := &networktypes.NetworkingConfig{ 696 EndpointsConfig: make(map[string]*networktypes.EndpointSettings), 697 } 698 699 networkingConfig.EndpointsConfig, err = parseNetworkOpts(copts) 700 if err != nil { 701 return nil, err 702 } 703 704 return &containerConfig{ 705 Config: config, 706 HostConfig: hostConfig, 707 NetworkingConfig: networkingConfig, 708 }, nil 709 } 710 711 // parseNetworkOpts converts --network advanced options to endpoint-specs, and combines 712 // them with the old --network-alias and --links. If returns an error if conflicting options 713 // are found. 714 // 715 // this function may return _multiple_ endpoints, which is not currently supported 716 // by the daemon, but may be in future; it's up to the daemon to produce an error 717 // in case that is not supported. 718 func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.EndpointSettings, error) { 719 var ( 720 endpoints = make(map[string]*networktypes.EndpointSettings, len(copts.netMode.Value())) 721 hasUserDefined, hasNonUserDefined bool 722 ) 723 724 for i, n := range copts.netMode.Value() { 725 n := n 726 if container.NetworkMode(n.Target).IsUserDefined() { 727 hasUserDefined = true 728 } else { 729 hasNonUserDefined = true 730 } 731 if i == 0 { 732 // The first network corresponds with what was previously the "only" 733 // network, and what would be used when using the non-advanced syntax 734 // `--network-alias`, `--link`, `--ip`, `--ip6`, and `--link-local-ip` 735 // are set on this network, to preserve backward compatibility with 736 // the non-advanced notation 737 if err := applyContainerOptions(&n, copts); err != nil { 738 return nil, err 739 } 740 } 741 ep, err := parseNetworkAttachmentOpt(n) 742 if err != nil { 743 return nil, err 744 } 745 if _, ok := endpoints[n.Target]; ok { 746 return nil, errdefs.InvalidParameter(errors.Errorf("network %q is specified multiple times", n.Target)) 747 } 748 749 // For backward compatibility: if no custom options are provided for the network, 750 // and only a single network is specified, omit the endpoint-configuration 751 // on the client (the daemon will still create it when creating the container) 752 if i == 0 && len(copts.netMode.Value()) == 1 { 753 if ep == nil || reflect.DeepEqual(*ep, networktypes.EndpointSettings{}) { 754 continue 755 } 756 } 757 endpoints[n.Target] = ep 758 } 759 if hasUserDefined && hasNonUserDefined { 760 return nil, errdefs.InvalidParameter(errors.New("conflicting options: cannot attach both user-defined and non-user-defined network-modes")) 761 } 762 return endpoints, nil 763 } 764 765 func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOptions) error { 766 // TODO should copts.MacAddress actually be set on the first network? (currently it's not) 767 // TODO should we error if _any_ advanced option is used? (i.e. forbid to combine advanced notation with the "old" flags (`--network-alias`, `--link`, `--ip`, `--ip6`)? 768 if len(n.Aliases) > 0 && copts.aliases.Len() > 0 { 769 return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --network-alias and per-network alias")) 770 } 771 if len(n.Links) > 0 && copts.links.Len() > 0 { 772 return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --link and per-network links")) 773 } 774 if n.IPv4Address != "" && copts.ipv4Address != "" { 775 return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address")) 776 } 777 if n.IPv6Address != "" && copts.ipv6Address != "" { 778 return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address")) 779 } 780 if copts.aliases.Len() > 0 { 781 n.Aliases = make([]string, copts.aliases.Len()) 782 copy(n.Aliases, copts.aliases.GetAll()) 783 } 784 if copts.links.Len() > 0 { 785 n.Links = make([]string, copts.links.Len()) 786 copy(n.Links, copts.links.GetAll()) 787 } 788 if copts.ipv4Address != "" { 789 n.IPv4Address = copts.ipv4Address 790 } 791 if copts.ipv6Address != "" { 792 n.IPv6Address = copts.ipv6Address 793 } 794 795 // TODO should linkLocalIPs be added to the _first_ network only, or to _all_ networks? (should this be a per-network option as well?) 796 if copts.linkLocalIPs.Len() > 0 { 797 n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) 798 copy(n.LinkLocalIPs, copts.linkLocalIPs.GetAll()) 799 } 800 return nil 801 } 802 803 func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.EndpointSettings, error) { 804 if strings.TrimSpace(ep.Target) == "" { 805 return nil, errors.New("no name set for network") 806 } 807 if !container.NetworkMode(ep.Target).IsUserDefined() { 808 if len(ep.Aliases) > 0 { 809 return nil, errors.New("network-scoped aliases are only supported for user-defined networks") 810 } 811 if len(ep.Links) > 0 { 812 return nil, errors.New("links are only supported for user-defined networks") 813 } 814 } 815 816 epConfig := &networktypes.EndpointSettings{} 817 epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...) 818 if len(ep.DriverOpts) > 0 { 819 epConfig.DriverOpts = make(map[string]string) 820 epConfig.DriverOpts = ep.DriverOpts 821 } 822 if len(ep.Links) > 0 { 823 epConfig.Links = ep.Links 824 } 825 if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 { 826 epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{ 827 IPv4Address: ep.IPv4Address, 828 IPv6Address: ep.IPv6Address, 829 LinkLocalIPs: ep.LinkLocalIPs, 830 } 831 } 832 return epConfig, nil 833 } 834 835 func convertToStandardNotation(ports []string) ([]string, error) { 836 optsList := []string{} 837 for _, publish := range ports { 838 if strings.Contains(publish, "=") { 839 params := map[string]string{"protocol": "tcp"} 840 for _, param := range strings.Split(publish, ",") { 841 opt := strings.Split(param, "=") 842 if len(opt) < 2 { 843 return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param) 844 } 845 846 params[opt[0]] = opt[1] 847 } 848 optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["published"], params["target"], params["protocol"])) 849 } else { 850 optsList = append(optsList, publish) 851 } 852 } 853 return optsList, nil 854 } 855 856 func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { 857 loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts) 858 if loggingDriver == "none" && len(loggingOpts) > 0 { 859 return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver) 860 } 861 return loggingOptsMap, nil 862 } 863 864 // takes a local seccomp daemon, reads the file contents for sending to the daemon 865 func parseSecurityOpts(securityOpts []string) ([]string, error) { 866 for key, opt := range securityOpts { 867 con := strings.SplitN(opt, "=", 2) 868 if len(con) == 1 && con[0] != "no-new-privileges" { 869 if strings.Contains(opt, ":") { 870 con = strings.SplitN(opt, ":", 2) 871 } else { 872 return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) 873 } 874 } 875 if con[0] == "seccomp" && con[1] != "unconfined" { 876 f, err := os.ReadFile(con[1]) 877 if err != nil { 878 return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) 879 } 880 b := bytes.NewBuffer(nil) 881 if err := json.Compact(b, f); err != nil { 882 return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) 883 } 884 securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) 885 } 886 } 887 888 return securityOpts, nil 889 } 890 891 // parseSystemPaths checks if `systempaths=unconfined` security option is set, 892 // and returns the `MaskedPaths` and `ReadonlyPaths` accordingly. An updated 893 // list of security options is returned with this option removed, because the 894 // `unconfined` option is handled client-side, and should not be sent to the 895 // daemon. 896 func parseSystemPaths(securityOpts []string) (filtered, maskedPaths, readonlyPaths []string) { 897 filtered = securityOpts[:0] 898 for _, opt := range securityOpts { 899 if opt == "systempaths=unconfined" { 900 maskedPaths = []string{} 901 readonlyPaths = []string{} 902 } else { 903 filtered = append(filtered, opt) 904 } 905 } 906 907 return filtered, maskedPaths, readonlyPaths 908 } 909 910 // parses storage options per container into a map 911 func parseStorageOpts(storageOpts []string) (map[string]string, error) { 912 m := make(map[string]string) 913 for _, option := range storageOpts { 914 if strings.Contains(option, "=") { 915 opt := strings.SplitN(option, "=", 2) 916 m[opt[0]] = opt[1] 917 } else { 918 return nil, errors.Errorf("invalid storage option") 919 } 920 } 921 return m, nil 922 } 923 924 // parseDevice parses a device mapping string to a container.DeviceMapping struct 925 func parseDevice(device, serverOS string) (container.DeviceMapping, error) { 926 switch serverOS { 927 case "linux": 928 return parseLinuxDevice(device) 929 case "windows": 930 return parseWindowsDevice(device) 931 } 932 return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS) 933 } 934 935 // parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct 936 // knowing that the target is a Linux daemon 937 func parseLinuxDevice(device string) (container.DeviceMapping, error) { 938 var src, dst string 939 permissions := "rwm" 940 arr := strings.Split(device, ":") 941 switch len(arr) { 942 case 3: 943 permissions = arr[2] 944 fallthrough 945 case 2: 946 if validDeviceMode(arr[1]) { 947 permissions = arr[1] 948 } else { 949 dst = arr[1] 950 } 951 fallthrough 952 case 1: 953 src = arr[0] 954 default: 955 return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device) 956 } 957 958 if dst == "" { 959 dst = src 960 } 961 962 deviceMapping := container.DeviceMapping{ 963 PathOnHost: src, 964 PathInContainer: dst, 965 CgroupPermissions: permissions, 966 } 967 return deviceMapping, nil 968 } 969 970 // parseWindowsDevice parses a device mapping string to a container.DeviceMapping struct 971 // knowing that the target is a Windows daemon 972 func parseWindowsDevice(device string) (container.DeviceMapping, error) { 973 return container.DeviceMapping{PathOnHost: device}, nil 974 } 975 976 // validateDeviceCgroupRule validates a device cgroup rule string format 977 // It will make sure 'val' is in the form: 978 // 979 // 'type major:minor mode' 980 func validateDeviceCgroupRule(val string) (string, error) { 981 if deviceCgroupRuleRegexp.MatchString(val) { 982 return val, nil 983 } 984 985 return val, errors.Errorf("invalid device cgroup format '%s'", val) 986 } 987 988 // validDeviceMode checks if the mode for device is valid or not. 989 // Valid mode is a composition of r (read), w (write), and m (mknod). 990 func validDeviceMode(mode string) bool { 991 var legalDeviceMode = map[rune]bool{ 992 'r': true, 993 'w': true, 994 'm': true, 995 } 996 if mode == "" { 997 return false 998 } 999 for _, c := range mode { 1000 if !legalDeviceMode[c] { 1001 return false 1002 } 1003 legalDeviceMode[c] = false 1004 } 1005 return true 1006 } 1007 1008 // validateDevice validates a path for devices 1009 func validateDevice(val string, serverOS string) (string, error) { 1010 switch serverOS { 1011 case "linux": 1012 return validateLinuxPath(val, validDeviceMode) 1013 case "windows": 1014 // Windows does validation entirely server-side 1015 return val, nil 1016 } 1017 return "", errors.Errorf("unknown server OS: %s", serverOS) 1018 } 1019 1020 // validateLinuxPath is the implementation of validateDevice knowing that the 1021 // target server operating system is a Linux daemon. 1022 // It will make sure 'val' is in the form: 1023 // 1024 // [host-dir:]container-path[:mode] 1025 // 1026 // It also validates the device mode. 1027 func validateLinuxPath(val string, validator func(string) bool) (string, error) { 1028 var containerPath string 1029 var mode string 1030 1031 if strings.Count(val, ":") > 2 { 1032 return val, errors.Errorf("bad format for path: %s", val) 1033 } 1034 1035 split := strings.SplitN(val, ":", 3) 1036 if split[0] == "" { 1037 return val, errors.Errorf("bad format for path: %s", val) 1038 } 1039 switch len(split) { 1040 case 1: 1041 containerPath = split[0] 1042 val = path.Clean(containerPath) 1043 case 2: 1044 if isValid := validator(split[1]); isValid { 1045 containerPath = split[0] 1046 mode = split[1] 1047 val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode) 1048 } else { 1049 containerPath = split[1] 1050 val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath)) 1051 } 1052 case 3: 1053 containerPath = split[1] 1054 mode = split[2] 1055 if isValid := validator(split[2]); !isValid { 1056 return val, errors.Errorf("bad mode specified: %s", mode) 1057 } 1058 val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) 1059 } 1060 1061 if !path.IsAbs(containerPath) { 1062 return val, errors.Errorf("%s is not an absolute path", containerPath) 1063 } 1064 return val, nil 1065 } 1066 1067 // validateAttach validates that the specified string is a valid attach option. 1068 func validateAttach(val string) (string, error) { 1069 s := strings.ToLower(val) 1070 for _, str := range []string{"stdin", "stdout", "stderr"} { 1071 if s == str { 1072 return s, nil 1073 } 1074 } 1075 return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") 1076 } 1077 1078 func validateAPIVersion(c *containerConfig, serverAPIVersion string) error { 1079 for _, m := range c.HostConfig.Mounts { 1080 if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") { 1081 return errors.Errorf("bind-nonrecursive requires API v1.40 or later") 1082 } 1083 } 1084 return nil 1085 }