github.com/hspak/nomad@v0.7.2-0.20180309000617-bc4ae22a39a5/client/driver/docker.go (about) 1 package driver 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log" 8 "net" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "strconv" 14 "strings" 15 "sync" 16 "syscall" 17 "time" 18 19 "github.com/armon/circbuf" 20 docker "github.com/fsouza/go-dockerclient" 21 22 "github.com/docker/docker/cli/config/configfile" 23 "github.com/docker/docker/reference" 24 "github.com/docker/docker/registry" 25 26 "github.com/hashicorp/go-multierror" 27 "github.com/hashicorp/go-plugin" 28 "github.com/hashicorp/nomad/client/allocdir" 29 "github.com/hashicorp/nomad/client/driver/env" 30 "github.com/hashicorp/nomad/client/driver/executor" 31 dstructs "github.com/hashicorp/nomad/client/driver/structs" 32 cstructs "github.com/hashicorp/nomad/client/structs" 33 "github.com/hashicorp/nomad/helper" 34 "github.com/hashicorp/nomad/helper/fields" 35 shelpers "github.com/hashicorp/nomad/helper/stats" 36 "github.com/hashicorp/nomad/nomad/structs" 37 "github.com/mitchellh/mapstructure" 38 ) 39 40 var ( 41 // We store the clients globally to cache the connection to the docker daemon. 42 createClients sync.Once 43 44 // client is a docker client with a timeout of 1 minute. This is for doing 45 // all operations with the docker daemon besides which are not long running 46 // such as creating, killing containers, etc. 47 client *docker.Client 48 49 // waitClient is a docker client with no timeouts. This is used for long 50 // running operations such as waiting on containers and collect stats 51 waitClient *docker.Client 52 53 // The statistics the Docker driver exposes 54 DockerMeasuredMemStats = []string{"RSS", "Cache", "Swap", "Max Usage"} 55 DockerMeasuredCpuStats = []string{"Throttled Periods", "Throttled Time", "Percent"} 56 57 // recoverableErrTimeouts returns a recoverable error if the error was due 58 // to timeouts 59 recoverableErrTimeouts = func(err error) error { 60 r := false 61 if strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") || 62 strings.Contains(err.Error(), "EOF") { 63 r = true 64 } 65 return structs.NewRecoverableError(err, r) 66 } 67 ) 68 69 const ( 70 // NoSuchContainerError is returned by the docker daemon if the container 71 // does not exist. 72 NoSuchContainerError = "No such container" 73 74 // The key populated in Node Attributes to indicate presence of the Docker 75 // driver 76 dockerDriverAttr = "driver.docker" 77 78 // dockerSELinuxLabelConfigOption is the key for configuring the 79 // SELinux label for binds. 80 dockerSELinuxLabelConfigOption = "docker.volumes.selinuxlabel" 81 82 // dockerVolumesConfigOption is the key for enabling the use of custom 83 // bind volumes to arbitrary host paths. 84 dockerVolumesConfigOption = "docker.volumes.enabled" 85 dockerVolumesConfigDefault = true 86 87 // dockerPrivilegedConfigOption is the key for running containers in 88 // Docker's privileged mode. 89 dockerPrivilegedConfigOption = "docker.privileged.enabled" 90 91 // dockerCleanupImageConfigOption is the key for whether or not to 92 // cleanup images after the task exits. 93 dockerCleanupImageConfigOption = "docker.cleanup.image" 94 dockerCleanupImageConfigDefault = true 95 96 // dockerPullTimeoutConfigOption is the key for setting an images pull 97 // timeout 98 dockerImageRemoveDelayConfigOption = "docker.cleanup.image.delay" 99 dockerImageRemoveDelayConfigDefault = 3 * time.Minute 100 101 // dockerCapsWhitelistConfigOption is the key for setting the list of 102 // allowed Linux capabilities 103 dockerCapsWhitelistConfigOption = "docker.caps.whitelist" 104 dockerCapsWhitelistConfigDefault = dockerBasicCaps 105 106 // dockerTimeout is the length of time a request can be outstanding before 107 // it is timed out. 108 dockerTimeout = 5 * time.Minute 109 110 // dockerImageResKey is the CreatedResources key for docker images 111 dockerImageResKey = "image" 112 113 // dockerAuthHelperPrefix is the prefix to attach to the credential helper 114 // and should be found in the $PATH. Example: ${prefix-}${helper-name} 115 dockerAuthHelperPrefix = "docker-credential-" 116 117 // dockerBasicCaps is comma-separated list of Linux capabilities that are 118 // allowed by docker by default, as documented in 119 // https://docs.docker.com/engine/reference/run/#block-io-bandwidth-blkio-constraint 120 dockerBasicCaps = "CHOWN,DAC_OVERRIDE,FSETID,FOWNER,MKNOD,NET_RAW,SETGID," + 121 "SETUID,SETFCAP,SETPCAP,NET_BIND_SERVICE,SYS_CHROOT,KILL,AUDIT_WRITE" 122 123 // This is cpu.cfs_period_us: the length of a period. 124 // The default values is 100 milliseconds (ms) represented in microseconds (us). 125 // Below is the documnentation: 126 // https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt 127 // https://docs.docker.com/engine/api/v1.35/# 128 defaultCFSPeriodUS = 100000 129 ) 130 131 type DockerDriver struct { 132 DriverContext 133 134 driverConfig *DockerDriverConfig 135 imageID string 136 137 // A tri-state boolean to know if the fingerprinting has happened and 138 // whether it has been successful 139 fingerprintSuccess *bool 140 } 141 142 type DockerDriverAuth struct { 143 Username string `mapstructure:"username"` // username for the registry 144 Password string `mapstructure:"password"` // password to access the registry 145 Email string `mapstructure:"email"` // email address of the user who is allowed to access the registry 146 ServerAddress string `mapstructure:"server_address"` // server address of the registry 147 } 148 149 type DockerLoggingOpts struct { 150 Type string `mapstructure:"type"` 151 ConfigRaw []map[string]string `mapstructure:"config"` 152 Config map[string]string `mapstructure:"-"` 153 } 154 155 type DockerMount struct { 156 Target string `mapstructure:"target"` 157 Source string `mapstructure:"source"` 158 ReadOnly bool `mapstructure:"readonly"` 159 VolumeOptions []*DockerVolumeOptions `mapstructure:"volume_options"` 160 } 161 162 type DockerDevice struct { 163 HostPath string `mapstructure:"host_path"` 164 ContainerPath string `mapstructure:"container_path"` 165 CgroupPermissions string `mapstructure:"cgroup_permissions"` 166 } 167 168 type DockerVolumeOptions struct { 169 NoCopy bool `mapstructure:"no_copy"` 170 Labels []map[string]string `mapstructure:"labels"` 171 DriverConfig []DockerVolumeDriverConfig `mapstructure:"driver_config"` 172 } 173 174 // VolumeDriverConfig holds a map of volume driver specific options 175 type DockerVolumeDriverConfig struct { 176 Name string `mapstructure:"name"` 177 Options []map[string]string `mapstructure:"options"` 178 } 179 180 // DockerDriverConfig defines the user specified config block in a jobspec 181 type DockerDriverConfig struct { 182 ImageName string `mapstructure:"image"` // Container's Image Name 183 LoadImage string `mapstructure:"load"` // LoadImage is a path to an image archive file 184 Command string `mapstructure:"command"` // The Command to run when the container starts up 185 Args []string `mapstructure:"args"` // The arguments to the Command 186 Entrypoint []string `mapstructure:"entrypoint"` // Override the containers entrypoint 187 IpcMode string `mapstructure:"ipc_mode"` // The IPC mode of the container - host and none 188 NetworkMode string `mapstructure:"network_mode"` // The network mode of the container - host, nat and none 189 NetworkAliases []string `mapstructure:"network_aliases"` // The network-scoped alias for the container 190 IPv4Address string `mapstructure:"ipv4_address"` // The container ipv4 address 191 IPv6Address string `mapstructure:"ipv6_address"` // the container ipv6 address 192 PidMode string `mapstructure:"pid_mode"` // The PID mode of the container - host and none 193 UTSMode string `mapstructure:"uts_mode"` // The UTS mode of the container - host and none 194 UsernsMode string `mapstructure:"userns_mode"` // The User namespace mode of the container - host and none 195 PortMapRaw []map[string]string `mapstructure:"port_map"` // 196 PortMap map[string]int `mapstructure:"-"` // A map of host port labels and the ports exposed on the container 197 Privileged bool `mapstructure:"privileged"` // Flag to run the container in privileged mode 198 SysctlRaw []map[string]string `mapstructure:"sysctl"` // 199 Sysctl map[string]string `mapstructure:"-"` // The sysctl custom configurations 200 UlimitRaw []map[string]string `mapstructure:"ulimit"` // 201 Ulimit []docker.ULimit `mapstructure:"-"` // The ulimit custom configurations 202 DNSServers []string `mapstructure:"dns_servers"` // DNS Server for containers 203 DNSSearchDomains []string `mapstructure:"dns_search_domains"` // DNS Search domains for containers 204 DNSOptions []string `mapstructure:"dns_options"` // DNS Options 205 ExtraHosts []string `mapstructure:"extra_hosts"` // Add host to /etc/hosts (host:IP) 206 Hostname string `mapstructure:"hostname"` // Hostname for containers 207 LabelsRaw []map[string]string `mapstructure:"labels"` // 208 Labels map[string]string `mapstructure:"-"` // Labels to set when the container starts up 209 Auth []DockerDriverAuth `mapstructure:"auth"` // Authentication credentials for a private Docker registry 210 AuthSoftFail bool `mapstructure:"auth_soft_fail"` // Soft-fail if auth creds are provided but fail 211 TTY bool `mapstructure:"tty"` // Allocate a Pseudo-TTY 212 Interactive bool `mapstructure:"interactive"` // Keep STDIN open even if not attached 213 ShmSize int64 `mapstructure:"shm_size"` // Size of /dev/shm of the container in bytes 214 WorkDir string `mapstructure:"work_dir"` // Working directory inside the container 215 Logging []DockerLoggingOpts `mapstructure:"logging"` // Logging options for syslog server 216 Volumes []string `mapstructure:"volumes"` // Host-Volumes to mount in, syntax: /path/to/host/directory:/destination/path/in/container 217 Mounts []DockerMount `mapstructure:"mounts"` // Docker volumes to mount 218 VolumeDriver string `mapstructure:"volume_driver"` // Docker volume driver used for the container's volumes 219 ForcePull bool `mapstructure:"force_pull"` // Always force pull before running image, useful if your tags are mutable 220 MacAddress string `mapstructure:"mac_address"` // Pin mac address to container 221 SecurityOpt []string `mapstructure:"security_opt"` // Flags to pass directly to security-opt 222 Devices []DockerDevice `mapstructure:"devices"` // To allow mounting USB or other serial control devices 223 CapAdd []string `mapstructure:"cap_add"` // Flags to pass directly to cap-add 224 CapDrop []string `mapstructure:"cap_drop"` // Flags to pass directly to cap-drop 225 ReadonlyRootfs bool `mapstructure:"readonly_rootfs"` // Mount the container’s root filesystem as read only 226 AdvertiseIPv6Address bool `mapstructure:"advertise_ipv6_address"` // Flag to use the GlobalIPv6Address from the container as the detected IP 227 CPUHardLimit bool `mapstructure:"cpu_hard_limit"` // Enforce CPU hard limit. 228 } 229 230 func sliceMergeUlimit(ulimitsRaw map[string]string) ([]docker.ULimit, error) { 231 var ulimits []docker.ULimit 232 233 for name, ulimitRaw := range ulimitsRaw { 234 if len(ulimitRaw) == 0 { 235 return []docker.ULimit{}, fmt.Errorf("Malformed ulimit specification %v: %q, cannot be empty", name, ulimitRaw) 236 } 237 // hard limit is optional 238 if strings.Contains(ulimitRaw, ":") == false { 239 ulimitRaw = ulimitRaw + ":" + ulimitRaw 240 } 241 242 splitted := strings.SplitN(ulimitRaw, ":", 2) 243 if len(splitted) < 2 { 244 return []docker.ULimit{}, fmt.Errorf("Malformed ulimit specification %v: %v", name, ulimitRaw) 245 } 246 soft, err := strconv.Atoi(splitted[0]) 247 if err != nil { 248 return []docker.ULimit{}, fmt.Errorf("Malformed soft ulimit %v: %v", name, ulimitRaw) 249 } 250 hard, err := strconv.Atoi(splitted[1]) 251 if err != nil { 252 return []docker.ULimit{}, fmt.Errorf("Malformed hard ulimit %v: %v", name, ulimitRaw) 253 } 254 255 ulimit := docker.ULimit{ 256 Name: name, 257 Soft: int64(soft), 258 Hard: int64(hard), 259 } 260 ulimits = append(ulimits, ulimit) 261 } 262 return ulimits, nil 263 } 264 265 // Validate validates a docker driver config 266 func (c *DockerDriverConfig) Validate() error { 267 if c.ImageName == "" { 268 return fmt.Errorf("Docker Driver needs an image name") 269 } 270 if len(c.Devices) > 0 { 271 for _, dev := range c.Devices { 272 if dev.HostPath == "" { 273 return fmt.Errorf("host path must be set in configuration for devices") 274 } 275 if dev.CgroupPermissions != "" { 276 for _, c := range dev.CgroupPermissions { 277 ch := string(c) 278 if ch != "r" && ch != "w" && ch != "m" { 279 return fmt.Errorf("invalid cgroup permission string: %q", dev.CgroupPermissions) 280 } 281 } 282 } 283 } 284 } 285 c.Sysctl = mapMergeStrStr(c.SysctlRaw...) 286 c.Labels = mapMergeStrStr(c.LabelsRaw...) 287 if len(c.Logging) > 0 { 288 c.Logging[0].Config = mapMergeStrStr(c.Logging[0].ConfigRaw...) 289 } 290 291 mergedUlimitsRaw := mapMergeStrStr(c.UlimitRaw...) 292 ulimit, err := sliceMergeUlimit(mergedUlimitsRaw) 293 if err != nil { 294 return err 295 } 296 c.Ulimit = ulimit 297 return nil 298 } 299 300 // NewDockerDriverConfig returns a docker driver config by parsing the HCL 301 // config 302 func NewDockerDriverConfig(task *structs.Task, env *env.TaskEnv) (*DockerDriverConfig, error) { 303 var dconf DockerDriverConfig 304 305 if err := mapstructure.WeakDecode(task.Config, &dconf); err != nil { 306 return nil, err 307 } 308 309 // Interpolate everything that is a string 310 dconf.ImageName = env.ReplaceEnv(dconf.ImageName) 311 dconf.Command = env.ReplaceEnv(dconf.Command) 312 dconf.Entrypoint = env.ParseAndReplace(dconf.Entrypoint) 313 dconf.IpcMode = env.ReplaceEnv(dconf.IpcMode) 314 dconf.NetworkMode = env.ReplaceEnv(dconf.NetworkMode) 315 dconf.NetworkAliases = env.ParseAndReplace(dconf.NetworkAliases) 316 dconf.IPv4Address = env.ReplaceEnv(dconf.IPv4Address) 317 dconf.IPv6Address = env.ReplaceEnv(dconf.IPv6Address) 318 dconf.PidMode = env.ReplaceEnv(dconf.PidMode) 319 dconf.UTSMode = env.ReplaceEnv(dconf.UTSMode) 320 dconf.Hostname = env.ReplaceEnv(dconf.Hostname) 321 dconf.WorkDir = env.ReplaceEnv(dconf.WorkDir) 322 dconf.LoadImage = env.ReplaceEnv(dconf.LoadImage) 323 dconf.Volumes = env.ParseAndReplace(dconf.Volumes) 324 dconf.VolumeDriver = env.ReplaceEnv(dconf.VolumeDriver) 325 dconf.DNSServers = env.ParseAndReplace(dconf.DNSServers) 326 dconf.DNSSearchDomains = env.ParseAndReplace(dconf.DNSSearchDomains) 327 dconf.DNSOptions = env.ParseAndReplace(dconf.DNSOptions) 328 dconf.ExtraHosts = env.ParseAndReplace(dconf.ExtraHosts) 329 dconf.MacAddress = env.ReplaceEnv(dconf.MacAddress) 330 dconf.SecurityOpt = env.ParseAndReplace(dconf.SecurityOpt) 331 dconf.CapAdd = env.ParseAndReplace(dconf.CapAdd) 332 dconf.CapDrop = env.ParseAndReplace(dconf.CapDrop) 333 334 for _, m := range dconf.SysctlRaw { 335 for k, v := range m { 336 delete(m, k) 337 m[env.ReplaceEnv(k)] = env.ReplaceEnv(v) 338 } 339 } 340 341 for _, m := range dconf.UlimitRaw { 342 for k, v := range m { 343 delete(m, k) 344 m[env.ReplaceEnv(k)] = env.ReplaceEnv(v) 345 } 346 } 347 348 for _, m := range dconf.LabelsRaw { 349 for k, v := range m { 350 delete(m, k) 351 m[env.ReplaceEnv(k)] = env.ReplaceEnv(v) 352 } 353 } 354 dconf.Labels = mapMergeStrStr(dconf.LabelsRaw...) 355 356 for i, a := range dconf.Auth { 357 dconf.Auth[i].Username = env.ReplaceEnv(a.Username) 358 dconf.Auth[i].Password = env.ReplaceEnv(a.Password) 359 dconf.Auth[i].Email = env.ReplaceEnv(a.Email) 360 dconf.Auth[i].ServerAddress = env.ReplaceEnv(a.ServerAddress) 361 } 362 363 for i, l := range dconf.Logging { 364 dconf.Logging[i].Type = env.ReplaceEnv(l.Type) 365 for _, c := range l.ConfigRaw { 366 for k, v := range c { 367 delete(c, k) 368 c[env.ReplaceEnv(k)] = env.ReplaceEnv(v) 369 } 370 } 371 } 372 373 for i, m := range dconf.Mounts { 374 dconf.Mounts[i].Target = env.ReplaceEnv(m.Target) 375 dconf.Mounts[i].Source = env.ReplaceEnv(m.Source) 376 377 if len(m.VolumeOptions) > 1 { 378 return nil, fmt.Errorf("Only one volume_options stanza allowed") 379 } 380 381 if len(m.VolumeOptions) == 1 { 382 vo := m.VolumeOptions[0] 383 if len(vo.Labels) > 1 { 384 return nil, fmt.Errorf("labels may only be specified once in volume_options stanza") 385 } 386 387 if len(vo.Labels) == 1 { 388 for k, v := range vo.Labels[0] { 389 if k != env.ReplaceEnv(k) { 390 delete(vo.Labels[0], k) 391 } 392 vo.Labels[0][env.ReplaceEnv(k)] = env.ReplaceEnv(v) 393 } 394 } 395 396 if len(vo.DriverConfig) > 1 { 397 return nil, fmt.Errorf("volume driver config may only be specified once") 398 } 399 if len(vo.DriverConfig) == 1 { 400 vo.DriverConfig[0].Name = env.ReplaceEnv(vo.DriverConfig[0].Name) 401 if len(vo.DriverConfig[0].Options) > 1 { 402 return nil, fmt.Errorf("volume driver options may only be specified once") 403 } 404 405 if len(vo.DriverConfig[0].Options) == 1 { 406 options := vo.DriverConfig[0].Options[0] 407 for k, v := range options { 408 if k != env.ReplaceEnv(k) { 409 delete(options, k) 410 } 411 options[env.ReplaceEnv(k)] = env.ReplaceEnv(v) 412 } 413 } 414 } 415 } 416 } 417 418 if len(dconf.Logging) > 0 { 419 dconf.Logging[0].Config = mapMergeStrStr(dconf.Logging[0].ConfigRaw...) 420 } 421 422 portMap := make(map[string]int) 423 for _, m := range dconf.PortMapRaw { 424 for k, v := range m { 425 ki, vi := env.ReplaceEnv(k), env.ReplaceEnv(v) 426 p, err := strconv.Atoi(vi) 427 if err != nil { 428 return nil, fmt.Errorf("failed to parse port map value %v to %v: %v", ki, vi, err) 429 } 430 portMap[ki] = p 431 } 432 } 433 dconf.PortMap = portMap 434 435 // Remove any http 436 if strings.Contains(dconf.ImageName, "https://") { 437 dconf.ImageName = strings.Replace(dconf.ImageName, "https://", "", 1) 438 } 439 440 // If devices are configured set default cgroup permissions 441 if len(dconf.Devices) > 0 { 442 for i, dev := range dconf.Devices { 443 if dev.CgroupPermissions == "" { 444 dev.CgroupPermissions = "rwm" 445 } 446 dconf.Devices[i] = dev 447 } 448 } 449 450 if err := dconf.Validate(); err != nil { 451 return nil, err 452 } 453 return &dconf, nil 454 } 455 456 type dockerPID struct { 457 Version string 458 Image string 459 ImageID string 460 ContainerID string 461 KillTimeout time.Duration 462 MaxKillTimeout time.Duration 463 PluginConfig *PluginReattachConfig 464 } 465 466 type DockerHandle struct { 467 pluginClient *plugin.Client 468 executor executor.Executor 469 client *docker.Client 470 waitClient *docker.Client 471 logger *log.Logger 472 Image string 473 ImageID string 474 containerID string 475 version string 476 killTimeout time.Duration 477 maxKillTimeout time.Duration 478 resourceUsageLock sync.RWMutex 479 resourceUsage *cstructs.TaskResourceUsage 480 waitCh chan *dstructs.WaitResult 481 doneCh chan bool 482 } 483 484 func NewDockerDriver(ctx *DriverContext) Driver { 485 return &DockerDriver{DriverContext: *ctx} 486 } 487 488 func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { 489 client, _, err := d.dockerClients() 490 if err != nil { 491 if d.fingerprintSuccess == nil || *d.fingerprintSuccess { 492 d.logger.Printf("[INFO] driver.docker: failed to initialize client: %s", err) 493 } 494 d.fingerprintSuccess = helper.BoolToPtr(false) 495 resp.RemoveAttribute(dockerDriverAttr) 496 return nil 497 } 498 499 // This is the first operation taken on the client so we'll try to 500 // establish a connection to the Docker daemon. If this fails it means 501 // Docker isn't available so we'll simply disable the docker driver. 502 env, err := client.Version() 503 if err != nil { 504 if d.fingerprintSuccess == nil || *d.fingerprintSuccess { 505 d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon at %s: %s", client.Endpoint(), err) 506 } 507 d.fingerprintSuccess = helper.BoolToPtr(false) 508 resp.RemoveAttribute(dockerDriverAttr) 509 return nil 510 } 511 512 resp.AddAttribute(dockerDriverAttr, "1") 513 resp.AddAttribute("driver.docker.version", env.Get("Version")) 514 resp.Detected = true 515 516 privileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) 517 if privileged { 518 resp.AddAttribute(dockerPrivilegedConfigOption, "1") 519 } 520 521 // Advertise if this node supports Docker volumes 522 if d.config.ReadBoolDefault(dockerVolumesConfigOption, dockerVolumesConfigDefault) { 523 resp.AddAttribute("driver."+dockerVolumesConfigOption, "1") 524 } 525 526 // Detect bridge IP address - #2785 527 if nets, err := client.ListNetworks(); err != nil { 528 d.logger.Printf("[WARN] driver.docker: error discovering bridge IP: %v", err) 529 } else { 530 for _, n := range nets { 531 if n.Name != "bridge" { 532 continue 533 } 534 535 if len(n.IPAM.Config) == 0 { 536 d.logger.Printf("[WARN] driver.docker: no IPAM config for bridge network") 537 break 538 } 539 540 if n.IPAM.Config[0].Gateway != "" { 541 resp.AddAttribute("driver.docker.bridge_ip", n.IPAM.Config[0].Gateway) 542 } else if d.fingerprintSuccess == nil { 543 // Docker 17.09.0-ce dropped the Gateway IP from the bridge network 544 // See https://github.com/moby/moby/issues/32648 545 d.logger.Printf("[DEBUG] driver.docker: bridge_ip could not be discovered") 546 } 547 break 548 } 549 } 550 551 d.fingerprintSuccess = helper.BoolToPtr(true) 552 return nil 553 } 554 555 // Validate is used to validate the driver configuration 556 func (d *DockerDriver) Validate(config map[string]interface{}) error { 557 fd := &fields.FieldData{ 558 Raw: config, 559 Schema: map[string]*fields.FieldSchema{ 560 "image": { 561 Type: fields.TypeString, 562 Required: true, 563 }, 564 "load": { 565 Type: fields.TypeString, 566 }, 567 "command": { 568 Type: fields.TypeString, 569 }, 570 "args": { 571 Type: fields.TypeArray, 572 }, 573 "entrypoint": { 574 Type: fields.TypeArray, 575 }, 576 "ipc_mode": { 577 Type: fields.TypeString, 578 }, 579 "network_mode": { 580 Type: fields.TypeString, 581 }, 582 "network_aliases": { 583 Type: fields.TypeArray, 584 }, 585 "ipv4_address": { 586 Type: fields.TypeString, 587 }, 588 "ipv6_address": { 589 Type: fields.TypeString, 590 }, 591 "mac_address": { 592 Type: fields.TypeString, 593 }, 594 "pid_mode": { 595 Type: fields.TypeString, 596 }, 597 "uts_mode": { 598 Type: fields.TypeString, 599 }, 600 "userns_mode": { 601 Type: fields.TypeString, 602 }, 603 "sysctl": { 604 Type: fields.TypeArray, 605 }, 606 "ulimit": { 607 Type: fields.TypeArray, 608 }, 609 "port_map": { 610 Type: fields.TypeArray, 611 }, 612 "privileged": { 613 Type: fields.TypeBool, 614 }, 615 "dns_servers": { 616 Type: fields.TypeArray, 617 }, 618 "dns_options": { 619 Type: fields.TypeArray, 620 }, 621 "dns_search_domains": { 622 Type: fields.TypeArray, 623 }, 624 "extra_hosts": { 625 Type: fields.TypeArray, 626 }, 627 "hostname": { 628 Type: fields.TypeString, 629 }, 630 "labels": { 631 Type: fields.TypeArray, 632 }, 633 "auth": { 634 Type: fields.TypeArray, 635 }, 636 "auth_soft_fail": { 637 Type: fields.TypeBool, 638 }, 639 // COMPAT: Remove in 0.6.0. SSL is no longer needed 640 "ssl": { 641 Type: fields.TypeBool, 642 }, 643 "tty": { 644 Type: fields.TypeBool, 645 }, 646 "interactive": { 647 Type: fields.TypeBool, 648 }, 649 "shm_size": { 650 Type: fields.TypeInt, 651 }, 652 "work_dir": { 653 Type: fields.TypeString, 654 }, 655 "logging": { 656 Type: fields.TypeArray, 657 }, 658 "volumes": { 659 Type: fields.TypeArray, 660 }, 661 "volume_driver": { 662 Type: fields.TypeString, 663 }, 664 "mounts": { 665 Type: fields.TypeArray, 666 }, 667 "force_pull": { 668 Type: fields.TypeBool, 669 }, 670 "security_opt": { 671 Type: fields.TypeArray, 672 }, 673 "devices": { 674 Type: fields.TypeArray, 675 }, 676 "cap_add": { 677 Type: fields.TypeArray, 678 }, 679 "cap_drop": { 680 Type: fields.TypeArray, 681 }, 682 "readonly_rootfs": { 683 Type: fields.TypeBool, 684 }, 685 "advertise_ipv6_address": { 686 Type: fields.TypeBool, 687 }, 688 "cpu_hard_limit": { 689 Type: fields.TypeBool, 690 }, 691 }, 692 } 693 694 if err := fd.Validate(); err != nil { 695 return err 696 } 697 698 return nil 699 } 700 701 func (d *DockerDriver) Abilities() DriverAbilities { 702 return DriverAbilities{ 703 SendSignals: true, 704 Exec: true, 705 } 706 } 707 708 func (d *DockerDriver) FSIsolation() cstructs.FSIsolation { 709 return cstructs.FSIsolationImage 710 } 711 712 // getDockerCoordinator returns the docker coordinator and the caller ID to use when 713 // interacting with the coordinator 714 func (d *DockerDriver) getDockerCoordinator(client *docker.Client) (*dockerCoordinator, string) { 715 config := &dockerCoordinatorConfig{ 716 client: client, 717 cleanup: d.config.ReadBoolDefault(dockerCleanupImageConfigOption, dockerCleanupImageConfigDefault), 718 logger: d.logger, 719 removeDelay: d.config.ReadDurationDefault(dockerImageRemoveDelayConfigOption, dockerImageRemoveDelayConfigDefault), 720 } 721 722 return GetDockerCoordinator(config), fmt.Sprintf("%s-%s", d.DriverContext.allocID, d.DriverContext.taskName) 723 } 724 725 func (d *DockerDriver) Prestart(ctx *ExecContext, task *structs.Task) (*PrestartResponse, error) { 726 driverConfig, err := NewDockerDriverConfig(task, ctx.TaskEnv) 727 if err != nil { 728 return nil, err 729 } 730 731 // Set state needed by Start 732 d.driverConfig = driverConfig 733 734 // Initialize docker API clients 735 client, _, err := d.dockerClients() 736 if err != nil { 737 return nil, fmt.Errorf("Failed to connect to docker daemon: %s", err) 738 } 739 740 // Ensure the image is available 741 id, err := d.createImage(driverConfig, client, ctx.TaskDir) 742 if err != nil { 743 return nil, err 744 } 745 d.imageID = id 746 747 resp := NewPrestartResponse() 748 resp.CreatedResources.Add(dockerImageResKey, id) 749 750 // Return the PortMap if it's set 751 if len(driverConfig.PortMap) > 0 { 752 resp.Network = &cstructs.DriverNetwork{ 753 PortMap: driverConfig.PortMap, 754 } 755 } 756 return resp, nil 757 } 758 759 func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, error) { 760 pluginLogFile := filepath.Join(ctx.TaskDir.Dir, "executor.out") 761 executorConfig := &dstructs.ExecutorConfig{ 762 LogFile: pluginLogFile, 763 LogLevel: d.config.LogLevel, 764 } 765 766 exec, pluginClient, err := createExecutor(d.config.LogOutput, d.config, executorConfig) 767 if err != nil { 768 return nil, err 769 } 770 executorCtx := &executor.ExecutorContext{ 771 TaskEnv: ctx.TaskEnv, 772 Task: task, 773 Driver: "docker", 774 LogDir: ctx.TaskDir.LogDir, 775 TaskDir: ctx.TaskDir.Dir, 776 PortLowerBound: d.config.ClientMinPort, 777 PortUpperBound: d.config.ClientMaxPort, 778 } 779 if err := exec.SetContext(executorCtx); err != nil { 780 pluginClient.Kill() 781 return nil, fmt.Errorf("failed to set executor context: %v", err) 782 } 783 784 // The user hasn't specified any logging options so launch our own syslog 785 // server if possible. 786 syslogAddr := "" 787 if len(d.driverConfig.Logging) == 0 { 788 if runtime.GOOS == "darwin" { 789 d.logger.Printf("[DEBUG] driver.docker: disabling syslog driver as Docker for Mac workaround") 790 } else { 791 ss, err := exec.LaunchSyslogServer() 792 if err != nil { 793 pluginClient.Kill() 794 return nil, fmt.Errorf("failed to start syslog collector: %v", err) 795 } 796 syslogAddr = ss.Addr 797 } 798 } 799 800 config, err := d.createContainerConfig(ctx, task, d.driverConfig, syslogAddr) 801 if err != nil { 802 d.logger.Printf("[ERR] driver.docker: failed to create container configuration for image %q (%q): %v", d.driverConfig.ImageName, d.imageID, err) 803 pluginClient.Kill() 804 return nil, fmt.Errorf("Failed to create container configuration for image %q (%q): %v", d.driverConfig.ImageName, d.imageID, err) 805 } 806 807 container, err := d.createContainer(config) 808 if err != nil { 809 wrapped := fmt.Sprintf("Failed to create container: %v", err) 810 d.logger.Printf("[ERR] driver.docker: %s", wrapped) 811 pluginClient.Kill() 812 return nil, structs.WrapRecoverable(wrapped, err) 813 } 814 815 d.logger.Printf("[INFO] driver.docker: created container %s", container.ID) 816 817 // We don't need to start the container if the container is already running 818 // since we don't create containers which are already present on the host 819 // and are running 820 if !container.State.Running { 821 // Start the container 822 if err := d.startContainer(container); err != nil { 823 d.logger.Printf("[ERR] driver.docker: failed to start container %s: %s", container.ID, err) 824 pluginClient.Kill() 825 return nil, structs.NewRecoverableError(fmt.Errorf("Failed to start container %s: %s", container.ID, err), structs.IsRecoverable(err)) 826 } 827 828 // InspectContainer to get all of the container metadata as 829 // much of the metadata (eg networking) isn't populated until 830 // the container is started 831 runningContainer, err := client.InspectContainer(container.ID) 832 if err != nil { 833 err = fmt.Errorf("failed to inspect started container %s: %s", container.ID, err) 834 d.logger.Printf("[ERR] driver.docker: %v", err) 835 pluginClient.Kill() 836 return nil, structs.NewRecoverableError(err, true) 837 } 838 container = runningContainer 839 d.logger.Printf("[INFO] driver.docker: started container %s", container.ID) 840 } else { 841 d.logger.Printf("[DEBUG] driver.docker: re-attaching to container %s with status %q", 842 container.ID, container.State.String()) 843 } 844 845 // Return a driver handle 846 maxKill := d.DriverContext.config.MaxKillTimeout 847 h := &DockerHandle{ 848 client: client, 849 waitClient: waitClient, 850 executor: exec, 851 pluginClient: pluginClient, 852 logger: d.logger, 853 Image: d.driverConfig.ImageName, 854 ImageID: d.imageID, 855 containerID: container.ID, 856 version: d.config.Version.VersionNumber(), 857 killTimeout: GetKillTimeout(task.KillTimeout, maxKill), 858 maxKillTimeout: maxKill, 859 doneCh: make(chan bool), 860 waitCh: make(chan *dstructs.WaitResult, 1), 861 } 862 go h.collectStats() 863 go h.run() 864 865 // Detect container address 866 ip, autoUse := d.detectIP(container) 867 868 // Create a response with the driver handle and container network metadata 869 resp := &StartResponse{ 870 Handle: h, 871 Network: &cstructs.DriverNetwork{ 872 PortMap: d.driverConfig.PortMap, 873 IP: ip, 874 AutoAdvertise: autoUse, 875 }, 876 } 877 return resp, nil 878 } 879 880 // detectIP of Docker container. Returns the first IP found as well as true if 881 // the IP should be advertised (bridge network IPs return false). Returns an 882 // empty string and false if no IP could be found. 883 func (d *DockerDriver) detectIP(c *docker.Container) (string, bool) { 884 if c.NetworkSettings == nil { 885 // This should only happen if there's been a coding error (such 886 // as not calling InspetContainer after CreateContainer). Code 887 // defensively in case the Docker API changes subtly. 888 d.logger.Printf("[ERROR] driver.docker: no network settings for container %s", c.ID) 889 return "", false 890 } 891 892 ip, ipName := "", "" 893 auto := false 894 for name, net := range c.NetworkSettings.Networks { 895 if net.IPAddress == "" { 896 // Ignore networks without an IP address 897 continue 898 } 899 900 ip = net.IPAddress 901 if d.driverConfig.AdvertiseIPv6Address { 902 ip = net.GlobalIPv6Address 903 auto = true 904 } 905 ipName = name 906 907 // Don't auto-advertise IPs for default networks (bridge on 908 // Linux, nat on Windows) 909 if name != "bridge" && name != "nat" { 910 auto = true 911 } 912 913 break 914 } 915 916 if n := len(c.NetworkSettings.Networks); n > 1 { 917 d.logger.Printf("[WARN] driver.docker: task %s multiple (%d) Docker networks for container %q but Nomad only supports 1: choosing %q", d.taskName, n, c.ID, ipName) 918 } 919 920 return ip, auto 921 } 922 923 func (d *DockerDriver) Cleanup(_ *ExecContext, res *CreatedResources) error { 924 retry := false 925 var merr multierror.Error 926 for key, resources := range res.Resources { 927 switch key { 928 case dockerImageResKey: 929 for _, value := range resources { 930 err := d.cleanupImage(value) 931 if err != nil { 932 if structs.IsRecoverable(err) { 933 retry = true 934 } 935 merr.Errors = append(merr.Errors, err) 936 continue 937 } 938 939 // Remove cleaned image from resources 940 res.Remove(dockerImageResKey, value) 941 } 942 default: 943 d.logger.Printf("[ERR] driver.docker: unknown resource to cleanup: %q", key) 944 } 945 } 946 return structs.NewRecoverableError(merr.ErrorOrNil(), retry) 947 } 948 949 // cleanupImage removes a Docker image. No error is returned if the image 950 // doesn't exist or is still in use. Requires the global client to already be 951 // initialized. 952 func (d *DockerDriver) cleanupImage(imageID string) error { 953 if !d.config.ReadBoolDefault(dockerCleanupImageConfigOption, dockerCleanupImageConfigDefault) { 954 // Config says not to cleanup 955 return nil 956 } 957 958 coordinator, callerID := d.getDockerCoordinator(client) 959 coordinator.RemoveImage(imageID, callerID) 960 961 return nil 962 } 963 964 // dockerClients creates two *docker.Client, one for long running operations and 965 // the other for shorter operations. In test / dev mode we can use ENV vars to 966 // connect to the docker daemon. In production mode we will read docker.endpoint 967 // from the config file. 968 func (d *DockerDriver) dockerClients() (*docker.Client, *docker.Client, error) { 969 if client != nil && waitClient != nil { 970 return client, waitClient, nil 971 } 972 973 var err error 974 var merr multierror.Error 975 createClients.Do(func() { 976 // Default to using whatever is configured in docker.endpoint. If this is 977 // not specified we'll fall back on NewClientFromEnv which reads config from 978 // the DOCKER_* environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and 979 // DOCKER_CERT_PATH. This allows us to lock down the config in production 980 // but also accept the standard ENV configs for dev and test. 981 dockerEndpoint := d.config.Read("docker.endpoint") 982 if dockerEndpoint != "" { 983 cert := d.config.Read("docker.tls.cert") 984 key := d.config.Read("docker.tls.key") 985 ca := d.config.Read("docker.tls.ca") 986 987 if cert+key+ca != "" { 988 d.logger.Printf("[DEBUG] driver.docker: using TLS client connection to %s", dockerEndpoint) 989 client, err = docker.NewTLSClient(dockerEndpoint, cert, key, ca) 990 if err != nil { 991 merr.Errors = append(merr.Errors, err) 992 } 993 waitClient, err = docker.NewTLSClient(dockerEndpoint, cert, key, ca) 994 if err != nil { 995 merr.Errors = append(merr.Errors, err) 996 } 997 } else { 998 d.logger.Printf("[DEBUG] driver.docker: using standard client connection to %s", dockerEndpoint) 999 client, err = docker.NewClient(dockerEndpoint) 1000 if err != nil { 1001 merr.Errors = append(merr.Errors, err) 1002 } 1003 waitClient, err = docker.NewClient(dockerEndpoint) 1004 if err != nil { 1005 merr.Errors = append(merr.Errors, err) 1006 } 1007 } 1008 client.SetTimeout(dockerTimeout) 1009 return 1010 } 1011 1012 d.logger.Println("[DEBUG] driver.docker: using client connection initialized from environment") 1013 client, err = docker.NewClientFromEnv() 1014 if err != nil { 1015 merr.Errors = append(merr.Errors, err) 1016 } 1017 client.SetTimeout(dockerTimeout) 1018 1019 waitClient, err = docker.NewClientFromEnv() 1020 if err != nil { 1021 merr.Errors = append(merr.Errors, err) 1022 } 1023 }) 1024 return client, waitClient, merr.ErrorOrNil() 1025 } 1026 1027 func (d *DockerDriver) containerBinds(driverConfig *DockerDriverConfig, ctx *ExecContext, 1028 task *structs.Task) ([]string, error) { 1029 1030 allocDirBind := fmt.Sprintf("%s:%s", ctx.TaskDir.SharedAllocDir, ctx.TaskEnv.EnvMap[env.AllocDir]) 1031 taskLocalBind := fmt.Sprintf("%s:%s", ctx.TaskDir.LocalDir, ctx.TaskEnv.EnvMap[env.TaskLocalDir]) 1032 secretDirBind := fmt.Sprintf("%s:%s", ctx.TaskDir.SecretsDir, ctx.TaskEnv.EnvMap[env.SecretsDir]) 1033 binds := []string{allocDirBind, taskLocalBind, secretDirBind} 1034 1035 volumesEnabled := d.config.ReadBoolDefault(dockerVolumesConfigOption, dockerVolumesConfigDefault) 1036 1037 if !volumesEnabled && driverConfig.VolumeDriver != "" { 1038 return nil, fmt.Errorf("%s is false; cannot use volume driver %q", dockerVolumesConfigOption, driverConfig.VolumeDriver) 1039 } 1040 1041 for _, userbind := range driverConfig.Volumes { 1042 parts := strings.Split(userbind, ":") 1043 if len(parts) < 2 { 1044 return nil, fmt.Errorf("invalid docker volume: %q", userbind) 1045 } 1046 1047 // Resolve dotted path segments 1048 parts[0] = filepath.Clean(parts[0]) 1049 1050 // Absolute paths aren't always supported 1051 if filepath.IsAbs(parts[0]) { 1052 if !volumesEnabled { 1053 // Disallow mounting arbitrary absolute paths 1054 return nil, fmt.Errorf("%s is false; cannot mount host paths: %+q", dockerVolumesConfigOption, userbind) 1055 } 1056 binds = append(binds, userbind) 1057 continue 1058 } 1059 1060 // Relative paths are always allowed as they mount within a container 1061 // When a VolumeDriver is set, we assume we receive a binding in the format volume-name:container-dest 1062 // Otherwise, we assume we receive a relative path binding in the format relative/to/task:/also/in/container 1063 if driverConfig.VolumeDriver == "" { 1064 // Expand path relative to alloc dir 1065 parts[0] = filepath.Join(ctx.TaskDir.Dir, parts[0]) 1066 } 1067 1068 binds = append(binds, strings.Join(parts, ":")) 1069 } 1070 1071 if selinuxLabel := d.config.Read(dockerSELinuxLabelConfigOption); selinuxLabel != "" { 1072 // Apply SELinux Label to each volume 1073 for i := range binds { 1074 binds[i] = fmt.Sprintf("%s:%s", binds[i], selinuxLabel) 1075 } 1076 } 1077 1078 return binds, nil 1079 } 1080 1081 // createContainerConfig initializes a struct needed to call docker.client.CreateContainer() 1082 func (d *DockerDriver) createContainerConfig(ctx *ExecContext, task *structs.Task, 1083 driverConfig *DockerDriverConfig, syslogAddr string) (docker.CreateContainerOptions, error) { 1084 var c docker.CreateContainerOptions 1085 if task.Resources == nil { 1086 // Guard against missing resources. We should never have been able to 1087 // schedule a job without specifying this. 1088 d.logger.Println("[ERR] driver.docker: task.Resources is empty") 1089 return c, fmt.Errorf("task.Resources is empty") 1090 } 1091 1092 binds, err := d.containerBinds(driverConfig, ctx, task) 1093 if err != nil { 1094 return c, err 1095 } 1096 1097 // create the config block that will later be consumed by go-dockerclient 1098 config := &docker.Config{ 1099 Image: d.imageID, 1100 Entrypoint: driverConfig.Entrypoint, 1101 Hostname: driverConfig.Hostname, 1102 User: task.User, 1103 Tty: driverConfig.TTY, 1104 OpenStdin: driverConfig.Interactive, 1105 StopTimeout: int(task.KillTimeout.Seconds()), 1106 StopSignal: task.KillSignal, 1107 } 1108 1109 if driverConfig.WorkDir != "" { 1110 config.WorkingDir = driverConfig.WorkDir 1111 } 1112 1113 memLimit := int64(task.Resources.MemoryMB) * 1024 * 1024 1114 1115 if len(driverConfig.Logging) == 0 { 1116 if runtime.GOOS == "darwin" { 1117 d.logger.Printf("[DEBUG] driver.docker: deferring logging to docker on Docker for Mac") 1118 } else { 1119 d.logger.Printf("[DEBUG] driver.docker: Setting default logging options to syslog and %s", syslogAddr) 1120 driverConfig.Logging = []DockerLoggingOpts{ 1121 {Type: "syslog", Config: map[string]string{"syslog-address": syslogAddr}}, 1122 } 1123 } 1124 } 1125 1126 hostConfig := &docker.HostConfig{ 1127 // Convert MB to bytes. This is an absolute value. 1128 Memory: memLimit, 1129 // Convert Mhz to shares. This is a relative value. 1130 CPUShares: int64(task.Resources.CPU), 1131 1132 // Binds are used to mount a host volume into the container. We mount a 1133 // local directory for storage and a shared alloc directory that can be 1134 // used to share data between different tasks in the same task group. 1135 Binds: binds, 1136 1137 VolumeDriver: driverConfig.VolumeDriver, 1138 } 1139 1140 // Calculate CPU Quota 1141 // cfs_quota_us is the time per core, so we must 1142 // multiply the time by the number of cores available 1143 // See https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-cpu 1144 if driverConfig.CPUHardLimit { 1145 numCores := runtime.NumCPU() 1146 percentTicks := float64(task.Resources.CPU) / float64(d.node.Resources.CPU) 1147 hostConfig.CPUQuota = int64(percentTicks*defaultCFSPeriodUS) * int64(numCores) 1148 } 1149 1150 // Windows does not support MemorySwap/MemorySwappiness #2193 1151 if runtime.GOOS == "windows" { 1152 hostConfig.MemorySwap = 0 1153 hostConfig.MemorySwappiness = -1 1154 } else { 1155 hostConfig.MemorySwap = memLimit // MemorySwap is memory + swap. 1156 } 1157 1158 if len(driverConfig.Logging) != 0 { 1159 d.logger.Printf("[DEBUG] driver.docker: Using config for logging: %+v", driverConfig.Logging[0]) 1160 hostConfig.LogConfig = docker.LogConfig{ 1161 Type: driverConfig.Logging[0].Type, 1162 Config: driverConfig.Logging[0].Config, 1163 } 1164 } 1165 1166 d.logger.Printf("[DEBUG] driver.docker: using %d bytes memory for %s", hostConfig.Memory, task.Name) 1167 d.logger.Printf("[DEBUG] driver.docker: using %d cpu shares for %s", hostConfig.CPUShares, task.Name) 1168 if driverConfig.CPUHardLimit { 1169 d.logger.Printf("[DEBUG] driver.docker: using %dms cpu quota and %dms cpu period for %s", hostConfig.CPUQuota, defaultCFSPeriodUS, task.Name) 1170 } 1171 d.logger.Printf("[DEBUG] driver.docker: binding directories %#v for %s", hostConfig.Binds, task.Name) 1172 1173 // set privileged mode 1174 hostPrivileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) 1175 if driverConfig.Privileged && !hostPrivileged { 1176 return c, fmt.Errorf(`Docker privileged mode is disabled on this Nomad agent`) 1177 } 1178 hostConfig.Privileged = driverConfig.Privileged 1179 1180 // set capabilities 1181 hostCapsWhitelistConfig := d.config.ReadDefault( 1182 dockerCapsWhitelistConfigOption, dockerCapsWhitelistConfigDefault) 1183 hostCapsWhitelist := make(map[string]struct{}) 1184 for _, cap := range strings.Split(hostCapsWhitelistConfig, ",") { 1185 cap = strings.ToLower(strings.TrimSpace(cap)) 1186 hostCapsWhitelist[cap] = struct{}{} 1187 } 1188 1189 if _, ok := hostCapsWhitelist["all"]; !ok { 1190 effectiveCaps, err := tweakCapabilities( 1191 strings.Split(dockerBasicCaps, ","), 1192 driverConfig.CapAdd, 1193 driverConfig.CapDrop, 1194 ) 1195 if err != nil { 1196 return c, err 1197 } 1198 var missingCaps []string 1199 for _, cap := range effectiveCaps { 1200 cap = strings.ToLower(cap) 1201 if _, ok := hostCapsWhitelist[cap]; !ok { 1202 missingCaps = append(missingCaps, cap) 1203 } 1204 } 1205 if len(missingCaps) > 0 { 1206 return c, fmt.Errorf("Docker driver doesn't have the following caps whitelisted on this Nomad agent: %s", missingCaps) 1207 } 1208 } 1209 1210 hostConfig.CapAdd = driverConfig.CapAdd 1211 hostConfig.CapDrop = driverConfig.CapDrop 1212 1213 // set SHM size 1214 if driverConfig.ShmSize != 0 { 1215 hostConfig.ShmSize = driverConfig.ShmSize 1216 } 1217 1218 // set DNS servers 1219 for _, ip := range driverConfig.DNSServers { 1220 if net.ParseIP(ip) != nil { 1221 hostConfig.DNS = append(hostConfig.DNS, ip) 1222 } else { 1223 d.logger.Printf("[ERR] driver.docker: invalid ip address for container dns server: %s", ip) 1224 } 1225 } 1226 1227 if len(driverConfig.Devices) > 0 { 1228 var devices []docker.Device 1229 for _, device := range driverConfig.Devices { 1230 dev := docker.Device{ 1231 PathOnHost: device.HostPath, 1232 PathInContainer: device.ContainerPath, 1233 CgroupPermissions: device.CgroupPermissions} 1234 devices = append(devices, dev) 1235 } 1236 hostConfig.Devices = devices 1237 } 1238 1239 // Setup mounts 1240 for _, m := range driverConfig.Mounts { 1241 hm := docker.HostMount{ 1242 Target: m.Target, 1243 Source: m.Source, 1244 Type: "volume", // Only type supported 1245 ReadOnly: m.ReadOnly, 1246 } 1247 if len(m.VolumeOptions) == 1 { 1248 vo := m.VolumeOptions[0] 1249 hm.VolumeOptions = &docker.VolumeOptions{ 1250 NoCopy: vo.NoCopy, 1251 } 1252 1253 if len(vo.DriverConfig) == 1 { 1254 dc := vo.DriverConfig[0] 1255 hm.VolumeOptions.DriverConfig = docker.VolumeDriverConfig{ 1256 Name: dc.Name, 1257 } 1258 if len(dc.Options) == 1 { 1259 hm.VolumeOptions.DriverConfig.Options = dc.Options[0] 1260 } 1261 } 1262 if len(vo.Labels) == 1 { 1263 hm.VolumeOptions.Labels = vo.Labels[0] 1264 } 1265 } 1266 hostConfig.Mounts = append(hostConfig.Mounts, hm) 1267 } 1268 1269 // set DNS search domains and extra hosts 1270 hostConfig.DNSSearch = driverConfig.DNSSearchDomains 1271 hostConfig.DNSOptions = driverConfig.DNSOptions 1272 hostConfig.ExtraHosts = driverConfig.ExtraHosts 1273 1274 hostConfig.IpcMode = driverConfig.IpcMode 1275 hostConfig.PidMode = driverConfig.PidMode 1276 hostConfig.UTSMode = driverConfig.UTSMode 1277 hostConfig.UsernsMode = driverConfig.UsernsMode 1278 hostConfig.SecurityOpt = driverConfig.SecurityOpt 1279 hostConfig.Sysctls = driverConfig.Sysctl 1280 hostConfig.Ulimits = driverConfig.Ulimit 1281 hostConfig.ReadonlyRootfs = driverConfig.ReadonlyRootfs 1282 1283 hostConfig.NetworkMode = driverConfig.NetworkMode 1284 if hostConfig.NetworkMode == "" { 1285 // docker default 1286 d.logger.Printf("[DEBUG] driver.docker: networking mode not specified; defaulting to %s", defaultNetworkMode) 1287 hostConfig.NetworkMode = defaultNetworkMode 1288 } 1289 1290 // Setup port mapping and exposed ports 1291 if len(task.Resources.Networks) == 0 { 1292 d.logger.Println("[DEBUG] driver.docker: No network interfaces are available") 1293 if len(driverConfig.PortMap) > 0 { 1294 return c, fmt.Errorf("Trying to map ports but no network interface is available") 1295 } 1296 } else { 1297 // TODO add support for more than one network 1298 network := task.Resources.Networks[0] 1299 publishedPorts := map[docker.Port][]docker.PortBinding{} 1300 exposedPorts := map[docker.Port]struct{}{} 1301 1302 for _, port := range network.ReservedPorts { 1303 // By default we will map the allocated port 1:1 to the container 1304 containerPortInt := port.Value 1305 1306 // If the user has mapped a port using port_map we'll change it here 1307 if mapped, ok := driverConfig.PortMap[port.Label]; ok { 1308 containerPortInt = mapped 1309 } 1310 1311 hostPortStr := strconv.Itoa(port.Value) 1312 containerPort := docker.Port(strconv.Itoa(containerPortInt)) 1313 1314 publishedPorts[containerPort+"/tcp"] = getPortBinding(network.IP, hostPortStr) 1315 publishedPorts[containerPort+"/udp"] = getPortBinding(network.IP, hostPortStr) 1316 d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d (static)", network.IP, port.Value, port.Value) 1317 1318 exposedPorts[containerPort+"/tcp"] = struct{}{} 1319 exposedPorts[containerPort+"/udp"] = struct{}{} 1320 d.logger.Printf("[DEBUG] driver.docker: exposed port %d", port.Value) 1321 } 1322 1323 for _, port := range network.DynamicPorts { 1324 // By default we will map the allocated port 1:1 to the container 1325 containerPortInt := port.Value 1326 1327 // If the user has mapped a port using port_map we'll change it here 1328 if mapped, ok := driverConfig.PortMap[port.Label]; ok { 1329 containerPortInt = mapped 1330 } 1331 1332 hostPortStr := strconv.Itoa(port.Value) 1333 containerPort := docker.Port(strconv.Itoa(containerPortInt)) 1334 1335 publishedPorts[containerPort+"/tcp"] = getPortBinding(network.IP, hostPortStr) 1336 publishedPorts[containerPort+"/udp"] = getPortBinding(network.IP, hostPortStr) 1337 d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d (mapped)", network.IP, port.Value, containerPortInt) 1338 1339 exposedPorts[containerPort+"/tcp"] = struct{}{} 1340 exposedPorts[containerPort+"/udp"] = struct{}{} 1341 d.logger.Printf("[DEBUG] driver.docker: exposed port %s", containerPort) 1342 } 1343 1344 hostConfig.PortBindings = publishedPorts 1345 config.ExposedPorts = exposedPorts 1346 } 1347 1348 parsedArgs := ctx.TaskEnv.ParseAndReplace(driverConfig.Args) 1349 1350 // If the user specified a custom command to run, we'll inject it here. 1351 if driverConfig.Command != "" { 1352 // Validate command 1353 if err := validateCommand(driverConfig.Command, "args"); err != nil { 1354 return c, err 1355 } 1356 1357 cmd := []string{driverConfig.Command} 1358 if len(driverConfig.Args) != 0 { 1359 cmd = append(cmd, parsedArgs...) 1360 } 1361 d.logger.Printf("[DEBUG] driver.docker: setting container startup command to: %s", strings.Join(cmd, " ")) 1362 config.Cmd = cmd 1363 } else if len(driverConfig.Args) != 0 { 1364 config.Cmd = parsedArgs 1365 } 1366 1367 if len(driverConfig.Labels) > 0 { 1368 config.Labels = driverConfig.Labels 1369 d.logger.Printf("[DEBUG] driver.docker: applied labels on the container: %+v", config.Labels) 1370 } 1371 1372 config.Env = ctx.TaskEnv.List() 1373 1374 containerName := fmt.Sprintf("%s-%s", task.Name, d.DriverContext.allocID) 1375 d.logger.Printf("[DEBUG] driver.docker: setting container name to: %s", containerName) 1376 1377 var networkingConfig *docker.NetworkingConfig 1378 if len(driverConfig.NetworkAliases) > 0 || driverConfig.IPv4Address != "" || driverConfig.IPv6Address != "" { 1379 networkingConfig = &docker.NetworkingConfig{ 1380 EndpointsConfig: map[string]*docker.EndpointConfig{ 1381 hostConfig.NetworkMode: {}, 1382 }, 1383 } 1384 } 1385 1386 if len(driverConfig.NetworkAliases) > 0 { 1387 networkingConfig.EndpointsConfig[hostConfig.NetworkMode].Aliases = driverConfig.NetworkAliases 1388 d.logger.Printf("[DEBUG] driver.docker: using network_mode %q with network aliases: %v", 1389 hostConfig.NetworkMode, strings.Join(driverConfig.NetworkAliases, ", ")) 1390 } 1391 1392 if driverConfig.IPv4Address != "" || driverConfig.IPv6Address != "" { 1393 networkingConfig.EndpointsConfig[hostConfig.NetworkMode].IPAMConfig = &docker.EndpointIPAMConfig{ 1394 IPv4Address: driverConfig.IPv4Address, 1395 IPv6Address: driverConfig.IPv6Address, 1396 } 1397 d.logger.Printf("[DEBUG] driver.docker: using network_mode %q with ipv4: %q and ipv6: %q", 1398 hostConfig.NetworkMode, driverConfig.IPv4Address, driverConfig.IPv6Address) 1399 } 1400 1401 if driverConfig.MacAddress != "" { 1402 config.MacAddress = driverConfig.MacAddress 1403 d.logger.Printf("[DEBUG] driver.docker: using pinned mac address: %q", config.MacAddress) 1404 } 1405 1406 return docker.CreateContainerOptions{ 1407 Name: containerName, 1408 Config: config, 1409 HostConfig: hostConfig, 1410 NetworkingConfig: networkingConfig, 1411 }, nil 1412 } 1413 1414 func (d *DockerDriver) Periodic() (bool, time.Duration) { 1415 return true, 15 * time.Second 1416 } 1417 1418 // createImage creates a docker image either by pulling it from a registry or by 1419 // loading it from the file system 1420 func (d *DockerDriver) createImage(driverConfig *DockerDriverConfig, client *docker.Client, taskDir *allocdir.TaskDir) (string, error) { 1421 image := driverConfig.ImageName 1422 repo, tag := docker.ParseRepositoryTag(image) 1423 if tag == "" { 1424 tag = "latest" 1425 } 1426 1427 coordinator, callerID := d.getDockerCoordinator(client) 1428 1429 // We're going to check whether the image is already downloaded. If the tag 1430 // is "latest", or ForcePull is set, we have to check for a new version every time so we don't 1431 // bother to check and cache the id here. We'll download first, then cache. 1432 if driverConfig.ForcePull { 1433 d.logger.Printf("[DEBUG] driver.docker: force pull image '%s:%s' instead of inspecting local", repo, tag) 1434 } else if tag != "latest" { 1435 if dockerImage, _ := client.InspectImage(image); dockerImage != nil { 1436 // Image exists so just increment its reference count 1437 coordinator.IncrementImageReference(dockerImage.ID, image, callerID) 1438 return dockerImage.ID, nil 1439 } 1440 } 1441 1442 // Load the image if specified 1443 if driverConfig.LoadImage != "" { 1444 return d.loadImage(driverConfig, client, taskDir) 1445 } 1446 1447 // Download the image 1448 return d.pullImage(driverConfig, client, repo, tag) 1449 } 1450 1451 // pullImage creates an image by pulling it from a docker registry 1452 func (d *DockerDriver) pullImage(driverConfig *DockerDriverConfig, client *docker.Client, repo, tag string) (id string, err error) { 1453 authOptions, err := d.resolveRegistryAuthentication(driverConfig, repo) 1454 if err != nil { 1455 if d.driverConfig.AuthSoftFail { 1456 d.logger.Printf("[WARN] Failed to find docker auth for repo %q: %v", repo, err) 1457 } else { 1458 return "", fmt.Errorf("Failed to find docker auth for repo %q: %v", repo, err) 1459 } 1460 } 1461 1462 if authIsEmpty(authOptions) { 1463 d.logger.Printf("[DEBUG] driver.docker: did not find docker auth for repo %q", repo) 1464 } 1465 1466 d.emitEvent("Downloading image %s:%s", repo, tag) 1467 coordinator, callerID := d.getDockerCoordinator(client) 1468 return coordinator.PullImage(driverConfig.ImageName, authOptions, callerID) 1469 } 1470 1471 // authBackend encapsulates a function that resolves registry credentials. 1472 type authBackend func(string) (*docker.AuthConfiguration, error) 1473 1474 // resolveRegistryAuthentication attempts to retrieve auth credentials for the 1475 // repo, trying all authentication-backends possible. 1476 func (d *DockerDriver) resolveRegistryAuthentication(driverConfig *DockerDriverConfig, repo string) (*docker.AuthConfiguration, error) { 1477 return firstValidAuth(repo, []authBackend{ 1478 authFromTaskConfig(driverConfig), 1479 authFromDockerConfig(d.config.Read("docker.auth.config")), 1480 authFromHelper(d.config.Read("docker.auth.helper")), 1481 }) 1482 } 1483 1484 // loadImage creates an image by loading it from the file system 1485 func (d *DockerDriver) loadImage(driverConfig *DockerDriverConfig, client *docker.Client, 1486 taskDir *allocdir.TaskDir) (id string, err error) { 1487 1488 archive := filepath.Join(taskDir.LocalDir, driverConfig.LoadImage) 1489 d.logger.Printf("[DEBUG] driver.docker: loading image from: %v", archive) 1490 1491 f, err := os.Open(archive) 1492 if err != nil { 1493 return "", fmt.Errorf("unable to open image archive: %v", err) 1494 } 1495 1496 if err := client.LoadImage(docker.LoadImageOptions{InputStream: f}); err != nil { 1497 return "", err 1498 } 1499 f.Close() 1500 1501 dockerImage, err := client.InspectImage(driverConfig.ImageName) 1502 if err != nil { 1503 return "", recoverableErrTimeouts(err) 1504 } 1505 1506 coordinator, callerID := d.getDockerCoordinator(client) 1507 coordinator.IncrementImageReference(dockerImage.ID, driverConfig.ImageName, callerID) 1508 return dockerImage.ID, nil 1509 } 1510 1511 // createContainer creates the container given the passed configuration. It 1512 // attempts to handle any transient Docker errors. 1513 func (d *DockerDriver) createContainer(config docker.CreateContainerOptions) (*docker.Container, error) { 1514 // Create a container 1515 attempted := 0 1516 CREATE: 1517 container, createErr := client.CreateContainer(config) 1518 if createErr == nil { 1519 return container, nil 1520 } 1521 1522 d.logger.Printf("[DEBUG] driver.docker: failed to create container %q from image %q (ID: %q) (attempt %d): %v", 1523 config.Name, d.driverConfig.ImageName, d.imageID, attempted+1, createErr) 1524 if strings.Contains(strings.ToLower(createErr.Error()), "container already exists") { 1525 containers, err := client.ListContainers(docker.ListContainersOptions{ 1526 All: true, 1527 }) 1528 if err != nil { 1529 d.logger.Printf("[ERR] driver.docker: failed to query list of containers matching name:%s", config.Name) 1530 return nil, recoverableErrTimeouts(fmt.Errorf("Failed to query list of containers: %s", err)) 1531 } 1532 1533 // Delete matching containers 1534 // Adding a / infront of the container name since Docker returns the 1535 // container names with a / pre-pended to the Nomad generated container names 1536 containerName := "/" + config.Name 1537 d.logger.Printf("[DEBUG] driver.docker: searching for container name %q to purge", containerName) 1538 for _, shimContainer := range containers { 1539 d.logger.Printf("[DEBUG] driver.docker: listed container %+v", shimContainer.Names) 1540 found := false 1541 for _, name := range shimContainer.Names { 1542 if name == containerName { 1543 d.logger.Printf("[DEBUG] driver.docker: Found container %v: %v", containerName, shimContainer.ID) 1544 found = true 1545 break 1546 } 1547 } 1548 1549 if !found { 1550 continue 1551 } 1552 1553 // Inspect the container and if the container isn't dead then return 1554 // the container 1555 container, err := client.InspectContainer(shimContainer.ID) 1556 if err != nil { 1557 err = fmt.Errorf("Failed to inspect container %s: %s", shimContainer.ID, err) 1558 1559 // This error is always recoverable as it could 1560 // be caused by races between listing 1561 // containers and this container being removed. 1562 // See #2802 1563 return nil, structs.NewRecoverableError(err, true) 1564 } 1565 if container != nil && container.State.Running { 1566 return container, nil 1567 } 1568 1569 err = client.RemoveContainer(docker.RemoveContainerOptions{ 1570 ID: container.ID, 1571 Force: true, 1572 }) 1573 if err != nil { 1574 d.logger.Printf("[ERR] driver.docker: failed to purge container %s", container.ID) 1575 return nil, recoverableErrTimeouts(fmt.Errorf("Failed to purge container %s: %s", container.ID, err)) 1576 } else if err == nil { 1577 d.logger.Printf("[INFO] driver.docker: purged container %s", container.ID) 1578 } 1579 } 1580 1581 if attempted < 5 { 1582 attempted++ 1583 time.Sleep(1 * time.Second) 1584 goto CREATE 1585 } 1586 } else if strings.Contains(strings.ToLower(createErr.Error()), "no such image") { 1587 // There is still a very small chance this is possible even with the 1588 // coordinator so retry. 1589 return nil, structs.NewRecoverableError(createErr, true) 1590 } 1591 1592 return nil, recoverableErrTimeouts(createErr) 1593 } 1594 1595 // startContainer starts the passed container. It attempts to handle any 1596 // transient Docker errors. 1597 func (d *DockerDriver) startContainer(c *docker.Container) error { 1598 // Start a container 1599 attempted := 0 1600 START: 1601 startErr := client.StartContainer(c.ID, c.HostConfig) 1602 if startErr == nil { 1603 return nil 1604 } 1605 1606 d.logger.Printf("[DEBUG] driver.docker: failed to start container %q (attempt %d): %v", c.ID, attempted+1, startErr) 1607 1608 // If it is a 500 error it is likely we can retry and be successful 1609 if strings.Contains(startErr.Error(), "API error (500)") { 1610 if attempted < 5 { 1611 attempted++ 1612 time.Sleep(1 * time.Second) 1613 goto START 1614 } 1615 return structs.NewRecoverableError(startErr, true) 1616 } 1617 1618 return recoverableErrTimeouts(startErr) 1619 } 1620 1621 func (d *DockerDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { 1622 // Split the handle 1623 pidBytes := []byte(strings.TrimPrefix(handleID, "DOCKER:")) 1624 pid := &dockerPID{} 1625 if err := json.Unmarshal(pidBytes, pid); err != nil { 1626 return nil, fmt.Errorf("Failed to parse handle '%s': %v", handleID, err) 1627 } 1628 d.logger.Printf("[INFO] driver.docker: re-attaching to docker process: %s", pid.ContainerID) 1629 d.logger.Printf("[DEBUG] driver.docker: re-attached to handle: %s", handleID) 1630 pluginConfig := &plugin.ClientConfig{ 1631 Reattach: pid.PluginConfig.PluginConfig(), 1632 } 1633 1634 client, waitClient, err := d.dockerClients() 1635 if err != nil { 1636 return nil, fmt.Errorf("Failed to connect to docker daemon: %s", err) 1637 } 1638 1639 // Look for a running container with this ID 1640 containers, err := client.ListContainers(docker.ListContainersOptions{ 1641 Filters: map[string][]string{ 1642 "id": {pid.ContainerID}, 1643 }, 1644 }) 1645 if err != nil { 1646 return nil, fmt.Errorf("Failed to query for container %s: %v", pid.ContainerID, err) 1647 } 1648 1649 found := false 1650 for _, container := range containers { 1651 if container.ID == pid.ContainerID { 1652 found = true 1653 } 1654 } 1655 if !found { 1656 return nil, fmt.Errorf("Failed to find container %s", pid.ContainerID) 1657 } 1658 exec, pluginClient, err := createExecutorWithConfig(pluginConfig, d.config.LogOutput) 1659 if err != nil { 1660 d.logger.Printf("[INFO] driver.docker: couldn't re-attach to the plugin process: %v", err) 1661 d.logger.Printf("[DEBUG] driver.docker: stopping container %q", pid.ContainerID) 1662 if e := client.StopContainer(pid.ContainerID, uint(pid.KillTimeout.Seconds())); e != nil { 1663 d.logger.Printf("[DEBUG] driver.docker: couldn't stop container: %v", e) 1664 } 1665 return nil, err 1666 } 1667 1668 ver, _ := exec.Version() 1669 d.logger.Printf("[DEBUG] driver.docker: version of executor: %v", ver.Version) 1670 1671 // Increment the reference count since we successfully attached to this 1672 // container 1673 coordinator, callerID := d.getDockerCoordinator(client) 1674 coordinator.IncrementImageReference(pid.ImageID, pid.Image, callerID) 1675 1676 // Return a driver handle 1677 h := &DockerHandle{ 1678 client: client, 1679 waitClient: waitClient, 1680 executor: exec, 1681 pluginClient: pluginClient, 1682 logger: d.logger, 1683 Image: pid.Image, 1684 ImageID: pid.ImageID, 1685 containerID: pid.ContainerID, 1686 version: pid.Version, 1687 killTimeout: pid.KillTimeout, 1688 maxKillTimeout: pid.MaxKillTimeout, 1689 doneCh: make(chan bool), 1690 waitCh: make(chan *dstructs.WaitResult, 1), 1691 } 1692 go h.collectStats() 1693 go h.run() 1694 return h, nil 1695 } 1696 1697 func (h *DockerHandle) ID() string { 1698 // Return a handle to the PID 1699 pid := dockerPID{ 1700 Version: h.version, 1701 ContainerID: h.containerID, 1702 Image: h.Image, 1703 ImageID: h.ImageID, 1704 KillTimeout: h.killTimeout, 1705 MaxKillTimeout: h.maxKillTimeout, 1706 PluginConfig: NewPluginReattachConfig(h.pluginClient.ReattachConfig()), 1707 } 1708 data, err := json.Marshal(pid) 1709 if err != nil { 1710 h.logger.Printf("[ERR] driver.docker: failed to marshal docker PID to JSON: %s", err) 1711 } 1712 return fmt.Sprintf("DOCKER:%s", string(data)) 1713 } 1714 1715 func (h *DockerHandle) ContainerID() string { 1716 return h.containerID 1717 } 1718 1719 func (h *DockerHandle) WaitCh() chan *dstructs.WaitResult { 1720 return h.waitCh 1721 } 1722 1723 func (h *DockerHandle) Update(task *structs.Task) error { 1724 // Store the updated kill timeout. 1725 h.killTimeout = GetKillTimeout(task.KillTimeout, h.maxKillTimeout) 1726 if err := h.executor.UpdateTask(task); err != nil { 1727 h.logger.Printf("[DEBUG] driver.docker: failed to update log config: %v", err) 1728 } 1729 1730 // Update is not possible 1731 return nil 1732 } 1733 1734 func (h *DockerHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error) { 1735 fullCmd := make([]string, len(args)+1) 1736 fullCmd[0] = cmd 1737 copy(fullCmd[1:], args) 1738 createExecOpts := docker.CreateExecOptions{ 1739 AttachStdin: false, 1740 AttachStdout: true, 1741 AttachStderr: true, 1742 Tty: false, 1743 Cmd: fullCmd, 1744 Container: h.containerID, 1745 Context: ctx, 1746 } 1747 exec, err := h.client.CreateExec(createExecOpts) 1748 if err != nil { 1749 return nil, 0, err 1750 } 1751 1752 output, _ := circbuf.NewBuffer(int64(dstructs.CheckBufSize)) 1753 startOpts := docker.StartExecOptions{ 1754 Detach: false, 1755 Tty: false, 1756 OutputStream: output, 1757 ErrorStream: output, 1758 Context: ctx, 1759 } 1760 if err := client.StartExec(exec.ID, startOpts); err != nil { 1761 return nil, 0, err 1762 } 1763 res, err := client.InspectExec(exec.ID) 1764 if err != nil { 1765 return output.Bytes(), 0, err 1766 } 1767 return output.Bytes(), res.ExitCode, nil 1768 } 1769 1770 func (h *DockerHandle) Signal(s os.Signal) error { 1771 // Convert types 1772 sysSig, ok := s.(syscall.Signal) 1773 if !ok { 1774 return fmt.Errorf("Failed to determine signal number") 1775 } 1776 1777 // TODO When we expose signals we will need a mapping layer that converts 1778 // MacOS signals to the correct signal number for docker. Or we change the 1779 // interface to take a signal string and leave it up to driver to map? 1780 1781 dockerSignal := docker.Signal(sysSig) 1782 opts := docker.KillContainerOptions{ 1783 ID: h.containerID, 1784 Signal: dockerSignal, 1785 } 1786 return h.client.KillContainer(opts) 1787 1788 } 1789 1790 // Kill is used to terminate the task. This uses `docker stop -t killTimeout` 1791 func (h *DockerHandle) Kill() error { 1792 // Stop the container 1793 err := h.client.StopContainer(h.containerID, uint(h.killTimeout.Seconds())) 1794 if err != nil { 1795 h.executor.Exit() 1796 h.pluginClient.Kill() 1797 1798 // Container has already been removed. 1799 if strings.Contains(err.Error(), NoSuchContainerError) { 1800 h.logger.Printf("[DEBUG] driver.docker: attempted to stop non-existent container %s", h.containerID) 1801 return nil 1802 } 1803 h.logger.Printf("[ERR] driver.docker: failed to stop container %s: %v", h.containerID, err) 1804 return fmt.Errorf("Failed to stop container %s: %s", h.containerID, err) 1805 } 1806 h.logger.Printf("[INFO] driver.docker: stopped container %s", h.containerID) 1807 return nil 1808 } 1809 1810 func (h *DockerHandle) Stats() (*cstructs.TaskResourceUsage, error) { 1811 h.resourceUsageLock.RLock() 1812 defer h.resourceUsageLock.RUnlock() 1813 var err error 1814 if h.resourceUsage == nil { 1815 err = fmt.Errorf("stats collection hasn't started yet") 1816 } 1817 return h.resourceUsage, err 1818 } 1819 1820 func (h *DockerHandle) run() { 1821 // Wait for it... 1822 exitCode, werr := h.waitClient.WaitContainer(h.containerID) 1823 if werr != nil { 1824 h.logger.Printf("[ERR] driver.docker: failed to wait for %s; container already terminated", h.containerID) 1825 } 1826 1827 if exitCode != 0 { 1828 werr = fmt.Errorf("Docker container exited with non-zero exit code: %d", exitCode) 1829 } 1830 1831 container, ierr := h.waitClient.InspectContainer(h.containerID) 1832 if ierr != nil { 1833 h.logger.Printf("[ERR] driver.docker: failed to inspect container %s: %v", h.containerID, ierr) 1834 } else if container.State.OOMKilled { 1835 werr = fmt.Errorf("OOM Killed") 1836 } 1837 1838 close(h.doneCh) 1839 1840 // Shutdown the syslog collector 1841 if err := h.executor.Exit(); err != nil { 1842 h.logger.Printf("[ERR] driver.docker: failed to kill the syslog collector: %v", err) 1843 } 1844 h.pluginClient.Kill() 1845 1846 // Stop the container just incase the docker daemon's wait returned 1847 // incorrectly 1848 if err := h.client.StopContainer(h.containerID, 0); err != nil { 1849 _, noSuchContainer := err.(*docker.NoSuchContainer) 1850 _, containerNotRunning := err.(*docker.ContainerNotRunning) 1851 if !containerNotRunning && !noSuchContainer { 1852 h.logger.Printf("[ERR] driver.docker: error stopping container: %v", err) 1853 } 1854 } 1855 1856 // Remove the container 1857 if err := h.client.RemoveContainer(docker.RemoveContainerOptions{ID: h.containerID, RemoveVolumes: true, Force: true}); err != nil { 1858 h.logger.Printf("[ERR] driver.docker: error removing container: %v", err) 1859 } 1860 1861 // Send the results 1862 h.waitCh <- dstructs.NewWaitResult(exitCode, 0, werr) 1863 close(h.waitCh) 1864 } 1865 1866 // collectStats starts collecting resource usage stats of a docker container 1867 func (h *DockerHandle) collectStats() { 1868 statsCh := make(chan *docker.Stats) 1869 statsOpts := docker.StatsOptions{ID: h.containerID, Done: h.doneCh, Stats: statsCh, Stream: true} 1870 go func() { 1871 //TODO handle Stats error 1872 if err := h.waitClient.Stats(statsOpts); err != nil { 1873 h.logger.Printf("[DEBUG] driver.docker: error collecting stats from container %s: %v", h.containerID, err) 1874 } 1875 }() 1876 numCores := runtime.NumCPU() 1877 for { 1878 select { 1879 case s := <-statsCh: 1880 if s != nil { 1881 ms := &cstructs.MemoryStats{ 1882 RSS: s.MemoryStats.Stats.Rss, 1883 Cache: s.MemoryStats.Stats.Cache, 1884 Swap: s.MemoryStats.Stats.Swap, 1885 MaxUsage: s.MemoryStats.MaxUsage, 1886 Measured: DockerMeasuredMemStats, 1887 } 1888 1889 cs := &cstructs.CpuStats{ 1890 ThrottledPeriods: s.CPUStats.ThrottlingData.ThrottledPeriods, 1891 ThrottledTime: s.CPUStats.ThrottlingData.ThrottledTime, 1892 Measured: DockerMeasuredCpuStats, 1893 } 1894 1895 // Calculate percentage 1896 cs.Percent = calculatePercent( 1897 s.CPUStats.CPUUsage.TotalUsage, s.PreCPUStats.CPUUsage.TotalUsage, 1898 s.CPUStats.SystemCPUUsage, s.PreCPUStats.SystemCPUUsage, numCores) 1899 cs.SystemMode = calculatePercent( 1900 s.CPUStats.CPUUsage.UsageInKernelmode, s.PreCPUStats.CPUUsage.UsageInKernelmode, 1901 s.CPUStats.CPUUsage.TotalUsage, s.PreCPUStats.CPUUsage.TotalUsage, numCores) 1902 cs.UserMode = calculatePercent( 1903 s.CPUStats.CPUUsage.UsageInUsermode, s.PreCPUStats.CPUUsage.UsageInUsermode, 1904 s.CPUStats.CPUUsage.TotalUsage, s.PreCPUStats.CPUUsage.TotalUsage, numCores) 1905 cs.TotalTicks = (cs.Percent / 100) * shelpers.TotalTicksAvailable() / float64(numCores) 1906 1907 h.resourceUsageLock.Lock() 1908 h.resourceUsage = &cstructs.TaskResourceUsage{ 1909 ResourceUsage: &cstructs.ResourceUsage{ 1910 MemoryStats: ms, 1911 CpuStats: cs, 1912 }, 1913 Timestamp: s.Read.UTC().UnixNano(), 1914 } 1915 h.resourceUsageLock.Unlock() 1916 } 1917 case <-h.doneCh: 1918 return 1919 } 1920 } 1921 } 1922 1923 func calculatePercent(newSample, oldSample, newTotal, oldTotal uint64, cores int) float64 { 1924 numerator := newSample - oldSample 1925 denom := newTotal - oldTotal 1926 if numerator <= 0 || denom <= 0 { 1927 return 0.0 1928 } 1929 1930 return (float64(numerator) / float64(denom)) * float64(cores) * 100.0 1931 } 1932 1933 // loadDockerConfig loads the docker config at the specified path, returning an 1934 // error if it couldn't be read. 1935 func loadDockerConfig(file string) (*configfile.ConfigFile, error) { 1936 f, err := os.Open(file) 1937 if err != nil { 1938 return nil, fmt.Errorf("Failed to open auth config file: %v, error: %v", file, err) 1939 } 1940 defer f.Close() 1941 1942 cfile := new(configfile.ConfigFile) 1943 if err = cfile.LoadFromReader(f); err != nil { 1944 return nil, fmt.Errorf("Failed to parse auth config file: %v", err) 1945 } 1946 return cfile, nil 1947 } 1948 1949 // parseRepositoryInfo takes a repo and returns the Docker RepositoryInfo. This 1950 // is useful for interacting with a Docker config object. 1951 func parseRepositoryInfo(repo string) (*registry.RepositoryInfo, error) { 1952 name, err := reference.ParseNamed(repo) 1953 if err != nil { 1954 return nil, fmt.Errorf("Failed to parse named repo %q: %v", repo, err) 1955 } 1956 1957 repoInfo, err := registry.ParseRepositoryInfo(name) 1958 if err != nil { 1959 return nil, fmt.Errorf("Failed to parse repository: %v", err) 1960 } 1961 1962 return repoInfo, nil 1963 } 1964 1965 // firstValidAuth tries a list of auth backends, returning first error or AuthConfiguration 1966 func firstValidAuth(repo string, backends []authBackend) (*docker.AuthConfiguration, error) { 1967 for _, backend := range backends { 1968 auth, err := backend(repo) 1969 if auth != nil || err != nil { 1970 return auth, err 1971 } 1972 } 1973 return nil, nil 1974 } 1975 1976 // authFromTaskConfig generates an authBackend for any auth given in the task-configuration 1977 func authFromTaskConfig(driverConfig *DockerDriverConfig) authBackend { 1978 return func(string) (*docker.AuthConfiguration, error) { 1979 if len(driverConfig.Auth) == 0 { 1980 return nil, nil 1981 } 1982 auth := driverConfig.Auth[0] 1983 return &docker.AuthConfiguration{ 1984 Username: auth.Username, 1985 Password: auth.Password, 1986 Email: auth.Email, 1987 ServerAddress: auth.ServerAddress, 1988 }, nil 1989 } 1990 } 1991 1992 // authFromDockerConfig generate an authBackend for a dockercfg-compatible file. 1993 // The authBacken can either be from explicit auth definitions or via credential 1994 // helpers 1995 func authFromDockerConfig(file string) authBackend { 1996 return func(repo string) (*docker.AuthConfiguration, error) { 1997 if file == "" { 1998 return nil, nil 1999 } 2000 repoInfo, err := parseRepositoryInfo(repo) 2001 if err != nil { 2002 return nil, err 2003 } 2004 2005 cfile, err := loadDockerConfig(file) 2006 if err != nil { 2007 return nil, err 2008 } 2009 2010 return firstValidAuth(repo, []authBackend{ 2011 func(string) (*docker.AuthConfiguration, error) { 2012 dockerAuthConfig := registry.ResolveAuthConfig(cfile.AuthConfigs, repoInfo.Index) 2013 auth := &docker.AuthConfiguration{ 2014 Username: dockerAuthConfig.Username, 2015 Password: dockerAuthConfig.Password, 2016 Email: dockerAuthConfig.Email, 2017 ServerAddress: dockerAuthConfig.ServerAddress, 2018 } 2019 if authIsEmpty(auth) { 2020 return nil, nil 2021 } 2022 return auth, nil 2023 }, 2024 authFromHelper(cfile.CredentialHelpers[registry.GetAuthConfigKey(repoInfo.Index)]), 2025 authFromHelper(cfile.CredentialsStore), 2026 }) 2027 } 2028 } 2029 2030 // authFromHelper generates an authBackend for a docker-credentials-helper; 2031 // A script taking the requested domain on input, outputting JSON with 2032 // "Username" and "Secret" 2033 func authFromHelper(helperName string) authBackend { 2034 return func(repo string) (*docker.AuthConfiguration, error) { 2035 if helperName == "" { 2036 return nil, nil 2037 } 2038 helper := dockerAuthHelperPrefix + helperName 2039 cmd := exec.Command(helper, "get") 2040 2041 // Ensure that the HTTPs prefix exists 2042 if !strings.HasPrefix(repo, "https://") { 2043 repo = fmt.Sprintf("https://%s", repo) 2044 } 2045 2046 cmd.Stdin = strings.NewReader(repo) 2047 2048 output, err := cmd.Output() 2049 if err != nil { 2050 switch err.(type) { 2051 default: 2052 return nil, err 2053 case *exec.ExitError: 2054 return nil, fmt.Errorf("%s with input %q failed with stderr: %s", helper, repo, output) 2055 } 2056 } 2057 2058 var response map[string]string 2059 if err := json.Unmarshal(output, &response); err != nil { 2060 return nil, err 2061 } 2062 2063 auth := &docker.AuthConfiguration{ 2064 Username: response["Username"], 2065 Password: response["Secret"], 2066 } 2067 2068 if authIsEmpty(auth) { 2069 return nil, nil 2070 } 2071 return auth, nil 2072 } 2073 } 2074 2075 // authIsEmpty returns if auth is nil or an empty structure 2076 func authIsEmpty(auth *docker.AuthConfiguration) bool { 2077 if auth == nil { 2078 return false 2079 } 2080 return auth.Username == "" && 2081 auth.Password == "" && 2082 auth.Email == "" && 2083 auth.ServerAddress == "" 2084 }