github.com/ssube/gitlab-ci-multi-runner@v1.2.1-0.20160607142738-b8d1285632e6/executors/docker/executor_docker.go (about) 1 package docker 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "crypto/md5" 7 "errors" 8 "fmt" 9 "io" 10 "os" 11 "os/user" 12 "path" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/docker/docker/pkg/homedir" 20 "github.com/fsouza/go-dockerclient" 21 "gitlab.com/gitlab-org/gitlab-ci-multi-runner/common" 22 "gitlab.com/gitlab-org/gitlab-ci-multi-runner/executors" 23 "gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers" 24 docker_helpers "gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers/docker" 25 ) 26 27 type dockerOptions struct { 28 Image string `json:"image"` 29 Services []string `json:"services"` 30 } 31 32 type executor struct { 33 executors.AbstractExecutor 34 client docker_helpers.Client 35 builds []*docker.Container 36 services []*docker.Container 37 caches []*docker.Container 38 options dockerOptions 39 } 40 41 const PrebuiltArchive = "prebuilt.tar.gz" 42 43 func (s *executor) getServiceVariables() []string { 44 return s.Build.GetAllVariables().PublicOrInternal().StringList() 45 } 46 47 func (s *executor) getAuthConfig(imageName string) (docker.AuthConfiguration, error) { 48 homeDir := homedir.Get() 49 if s.Shell.User != "" { 50 u, err := user.Lookup(s.Shell.User) 51 if err != nil { 52 return docker.AuthConfiguration{}, err 53 } 54 homeDir = u.HomeDir 55 } 56 if homeDir == "" { 57 return docker.AuthConfiguration{}, fmt.Errorf("Failed to get home directory") 58 } 59 60 indexName, _ := docker_helpers.SplitDockerImageName(imageName) 61 62 authConfigs, err := docker_helpers.ReadDockerAuthConfigs(homeDir) 63 if err != nil { 64 // ignore doesn't exist errors 65 if os.IsNotExist(err) { 66 err = nil 67 } 68 return docker.AuthConfiguration{}, err 69 } 70 71 authConfig := docker_helpers.ResolveDockerAuthConfig(indexName, authConfigs) 72 if authConfig != nil { 73 s.Debugln("Using", authConfig.Username, "to connect to", authConfig.ServerAddress, "in order to resolve", imageName, "...") 74 return *authConfig, nil 75 } 76 77 return docker.AuthConfiguration{}, fmt.Errorf("No credentials found for %v", indexName) 78 } 79 80 func (s *executor) pullDockerImage(imageName string) (*docker.Image, error) { 81 s.Println("Pulling docker image", imageName, "...") 82 authConfig, err := s.getAuthConfig(imageName) 83 if err != nil { 84 s.Debugln(err) 85 } 86 87 pullImageOptions := docker.PullImageOptions{ 88 Repository: imageName, 89 } 90 91 // Add :latest to limit the download results 92 if !strings.ContainsAny(pullImageOptions.Repository, ":@") { 93 pullImageOptions.Repository += ":latest" 94 } 95 96 err = s.client.PullImage(pullImageOptions, authConfig) 97 if err != nil { 98 return nil, err 99 } 100 101 image, err := s.client.InspectImage(imageName) 102 return image, err 103 } 104 105 func (s *executor) getDockerImage(imageName string) (*docker.Image, error) { 106 pullPolicy, err := s.Config.Docker.PullPolicy.Get() 107 if err != nil { 108 return nil, err 109 } 110 111 s.Debugln("Looking for image", imageName, "...") 112 image, err := s.client.InspectImage(imageName) 113 114 // If never is specified then we return what inspect did return 115 if pullPolicy == common.DockerPullPolicyNever { 116 return image, err 117 } 118 119 if err == nil { 120 // Don't pull image that is passed by ID 121 if image.ID == imageName { 122 return image, nil 123 } 124 125 // If not-present is specified 126 if pullPolicy == common.DockerPullPolicyIfNotPresent { 127 return image, err 128 } 129 } 130 131 newImage, err := s.pullDockerImage(imageName) 132 if err != nil { 133 if image != nil { 134 s.Warningln("Cannot pull the latest version of image", imageName, ":", err) 135 s.Warningln("Locally found image will be used instead.") 136 return image, nil 137 } 138 return nil, err 139 } 140 return newImage, nil 141 } 142 143 func (s *executor) getPrebuiltImage(imageType string) (image *docker.Image, err error) { 144 imageName := "gitlab-runner-" + imageType + ":" + common.REVISION 145 s.Debugln("Looking for prebuilt image", imageName, "...") 146 image, err = s.client.InspectImage(imageName) 147 if err == nil { 148 return 149 } 150 151 data, err := Asset(PrebuiltArchive) 152 if err != nil { 153 return 154 } 155 156 gz, err := gzip.NewReader(bytes.NewReader(data)) 157 if err != nil { 158 return 159 } 160 defer gz.Close() 161 162 s.Debugln("Loading prebuilt image...") 163 err = s.client.LoadImage(docker.LoadImageOptions{ 164 InputStream: gz, 165 }) 166 if err != nil { 167 return 168 } 169 170 return s.client.InspectImage(imageName) 171 } 172 173 func (s *executor) getAbsoluteContainerPath(dir string) string { 174 if path.IsAbs(dir) { 175 return dir 176 } 177 return path.Join(s.Build.FullProjectDir(), dir) 178 } 179 180 func (s *executor) addHostVolume(binds *[]string, hostPath, containerPath string) error { 181 containerPath = s.getAbsoluteContainerPath(containerPath) 182 s.Debugln("Using host-based", hostPath, "for", containerPath, "...") 183 *binds = append(*binds, fmt.Sprintf("%v:%v", hostPath, containerPath)) 184 return nil 185 } 186 187 func (s *executor) getLabels(containerType string, otherLabels ...string) map[string]string { 188 labels := make(map[string]string) 189 labels[dockerLabelPrefix+".build.id"] = strconv.Itoa(s.Build.ID) 190 labels[dockerLabelPrefix+".build.sha"] = s.Build.Sha 191 labels[dockerLabelPrefix+".build.before_sha"] = s.Build.BeforeSha 192 labels[dockerLabelPrefix+".build.ref_name"] = s.Build.RefName 193 labels[dockerLabelPrefix+".project.id"] = strconv.Itoa(s.Build.ProjectID) 194 labels[dockerLabelPrefix+".runner.id"] = s.Build.Runner.ShortDescription() 195 labels[dockerLabelPrefix+".runner.local_id"] = strconv.Itoa(s.Build.RunnerID) 196 labels[dockerLabelPrefix+".type"] = containerType 197 for _, label := range otherLabels { 198 keyValue := strings.SplitN(label, "=", 2) 199 if len(keyValue) == 2 { 200 labels[dockerLabelPrefix+"."+keyValue[0]] = keyValue[1] 201 } 202 } 203 return labels 204 } 205 206 func (s *executor) createCacheVolume(containerName, containerPath string) (*docker.Container, error) { 207 // get busybox image 208 cacheImage, err := s.getPrebuiltImage("cache") 209 if err != nil { 210 return nil, err 211 } 212 213 createContainerOptions := docker.CreateContainerOptions{ 214 Name: containerName, 215 Config: &docker.Config{ 216 Image: cacheImage.ID, 217 Cmd: []string{ 218 containerPath, 219 }, 220 Volumes: map[string]struct{}{ 221 containerPath: {}, 222 }, 223 Labels: s.getLabels("cache", "cache.dir="+containerPath), 224 }, 225 HostConfig: &docker.HostConfig{ 226 LogConfig: docker.LogConfig{ 227 Type: "json-file", 228 }, 229 }, 230 } 231 232 container, err := s.client.CreateContainer(createContainerOptions) 233 if err != nil { 234 if container != nil { 235 go s.removeContainer(container.ID) 236 } 237 return nil, err 238 } 239 240 s.Debugln("Starting cache container", container.ID, "...") 241 err = s.client.StartContainer(container.ID, nil) 242 if err != nil { 243 go s.removeContainer(container.ID) 244 return nil, err 245 } 246 247 s.Debugln("Waiting for cache container", container.ID, "...") 248 errorCode, err := s.client.WaitContainer(container.ID) 249 if err != nil { 250 go s.removeContainer(container.ID) 251 return nil, err 252 } 253 254 if errorCode != 0 { 255 go s.removeContainer(container.ID) 256 return nil, fmt.Errorf("cache container for %s returned %d", containerPath, errorCode) 257 } 258 259 return container, nil 260 } 261 262 func (s *executor) addCacheVolume(binds, volumesFrom *[]string, containerPath string) error { 263 var err error 264 containerPath = s.getAbsoluteContainerPath(containerPath) 265 266 // disable cache for automatic container cache, but leave it for host volumes (they are shared on purpose) 267 if s.Config.Docker.DisableCache { 268 s.Debugln("Container cache for", containerPath, " is disabled.") 269 return nil 270 } 271 272 hash := md5.Sum([]byte(containerPath)) 273 274 // use host-based cache 275 if cacheDir := s.Config.Docker.CacheDir; cacheDir != "" { 276 hostPath := fmt.Sprintf("%s/%s/%x", cacheDir, s.Build.ProjectUniqueName(), hash) 277 hostPath, err := filepath.Abs(hostPath) 278 if err != nil { 279 return err 280 } 281 s.Debugln("Using path", hostPath, "as cache for", containerPath, "...") 282 *binds = append(*binds, fmt.Sprintf("%v:%v", filepath.ToSlash(hostPath), containerPath)) 283 return nil 284 } 285 286 // get existing cache container 287 containerName := fmt.Sprintf("%s-cache-%x", s.Build.ProjectUniqueName(), hash) 288 container, _ := s.client.InspectContainer(containerName) 289 290 // check if we have valid cache, if not remove the broken container 291 if container != nil && container.Volumes[containerPath] == "" { 292 s.removeContainer(container.ID) 293 container = nil 294 } 295 296 // create new cache container for that project 297 if container == nil { 298 container, err = s.createCacheVolume(containerName, containerPath) 299 if err != nil { 300 return err 301 } 302 } 303 304 s.Debugln("Using container", container.ID, "as cache", containerPath, "...") 305 *volumesFrom = append(*volumesFrom, container.ID) 306 return nil 307 } 308 309 func (s *executor) addVolume(binds, volumesFrom *[]string, volume string) error { 310 var err error 311 hostVolume := strings.SplitN(volume, ":", 2) 312 switch len(hostVolume) { 313 case 2: 314 err = s.addHostVolume(binds, hostVolume[0], hostVolume[1]) 315 316 case 1: 317 // disable cache disables 318 err = s.addCacheVolume(binds, volumesFrom, hostVolume[0]) 319 } 320 321 if err != nil { 322 s.Errorln("Failed to create container volume for", volume, err) 323 } 324 return err 325 } 326 327 func (s *executor) createVolumes() ([]string, []string, error) { 328 var binds, volumesFrom []string 329 330 for _, volume := range s.Config.Docker.Volumes { 331 s.addVolume(&binds, &volumesFrom, volume) 332 } 333 334 // Cache Git sources: 335 // take path of the projects directory, 336 // because we use `rm -rf` which could remove the mounted volume 337 parentDir := path.Dir(s.Build.FullProjectDir()) 338 339 // Caching is supported only for absolute and non-root paths 340 if path.IsAbs(parentDir) && parentDir != "/" { 341 if s.Build.AllowGitFetch && !s.Config.Docker.DisableCache { 342 // create persistent cache container 343 s.addVolume(&binds, &volumesFrom, parentDir) 344 } else { 345 // create temporary cache container 346 container, _ := s.createCacheVolume("", parentDir) 347 if container != nil { 348 s.caches = append(s.caches, container) 349 volumesFrom = append(volumesFrom, container.ID) 350 } 351 } 352 } 353 354 return binds, volumesFrom, nil 355 } 356 357 func (s *executor) parseDeviceString(deviceString string) (device docker.Device, err error) { 358 // Split the device string PathOnHost[:PathInContainer[:CgroupPermissions]] 359 parts := strings.Split(deviceString, ":") 360 361 if len(parts) > 3 { 362 err = fmt.Errorf("Too many colons") 363 return 364 } 365 366 device.PathOnHost = parts[0] 367 368 // Optional container path 369 if len(parts) >= 2 { 370 device.PathInContainer = parts[1] 371 } else { 372 // default: device at same path in container 373 device.PathInContainer = device.PathOnHost 374 } 375 376 // Optional permissions 377 if len(parts) >= 3 { 378 device.CgroupPermissions = parts[2] 379 } else { 380 // default: rwm, just like 'docker run' 381 device.CgroupPermissions = "rwm" 382 } 383 384 return 385 } 386 387 func (s *executor) createDevices() (devices []docker.Device, err error) { 388 for _, deviceString := range s.Config.Docker.Devices { 389 390 device, err := s.parseDeviceString(deviceString) 391 if err != nil { 392 err = fmt.Errorf("Failed to parse device string %q: %s", deviceString, err) 393 return nil, err 394 } 395 396 devices = append(devices, device) 397 } 398 return 399 } 400 401 func (s *executor) splitServiceAndVersion(serviceDescription string) (service string, version string, linkNames []string) { 402 splits := strings.SplitN(serviceDescription, ":", 2) 403 version = "latest" 404 switch len(splits) { 405 case 1: 406 service = splits[0] 407 408 case 2: 409 service = splits[0] 410 version = splits[1] 411 412 default: 413 return 414 } 415 416 linkName := strings.Replace(service, "/", "__", -1) 417 linkNames = append(linkNames, linkName) 418 419 // Create alternative link name according to RFC 1123 420 // Where you can use only `a-zA-Z0-9-` 421 if alternativeName := strings.Replace(service, "/", "-", -1); linkName != alternativeName { 422 linkNames = append(linkNames, alternativeName) 423 } 424 return 425 } 426 427 func (s *executor) createService(service, version string) (*docker.Container, error) { 428 if len(service) == 0 { 429 return nil, errors.New("invalid service name") 430 } 431 432 serviceImage, err := s.getDockerImage(service + ":" + version) 433 if err != nil { 434 return nil, err 435 } 436 437 containerName := s.Build.ProjectUniqueName() + "-" + strings.Replace(service, "/", "__", -1) 438 439 // this will fail potentially some builds if there's name collision 440 s.removeContainer(containerName) 441 442 s.Println("Starting service", service+":"+version, "...") 443 createContainerOpts := docker.CreateContainerOptions{ 444 Name: containerName, 445 Config: &docker.Config{ 446 Image: serviceImage.ID, 447 Labels: s.getLabels("service", "service="+service, "service.version="+version), 448 Env: s.getServiceVariables(), 449 }, 450 HostConfig: &docker.HostConfig{ 451 RestartPolicy: docker.NeverRestart(), 452 Privileged: s.Config.Docker.Privileged, 453 NetworkMode: s.Config.Docker.NetworkMode, 454 LogConfig: docker.LogConfig{ 455 Type: "json-file", 456 }, 457 }, 458 } 459 460 s.Debugln("Creating service container", createContainerOpts.Name, "...") 461 container, err := s.client.CreateContainer(createContainerOpts) 462 if err != nil { 463 return nil, err 464 } 465 466 s.Debugln("Starting service container", container.ID, "...") 467 err = s.client.StartContainer(container.ID, nil) 468 if err != nil { 469 go s.removeContainer(container.ID) 470 return nil, err 471 } 472 473 return container, nil 474 } 475 476 func (s *executor) getServiceNames() ([]string, error) { 477 services := s.Config.Docker.Services 478 479 for _, service := range s.options.Services { 480 service = s.Build.GetAllVariables().ExpandValue(service) 481 err := s.verifyAllowedImage(service, "services", s.Config.Docker.AllowedServices, s.Config.Docker.Services) 482 if err != nil { 483 return nil, err 484 } 485 486 services = append(services, service) 487 } 488 489 return services, nil 490 } 491 492 func (s *executor) waitForServices() { 493 waitForServicesTimeout := s.Config.Docker.WaitForServicesTimeout 494 if waitForServicesTimeout == 0 { 495 waitForServicesTimeout = common.DefaultWaitForServicesTimeout 496 } 497 498 // wait for all services to came up 499 if waitForServicesTimeout > 0 && len(s.services) > 0 { 500 s.Println("Waiting for services to be up and running...") 501 wg := sync.WaitGroup{} 502 for _, service := range s.services { 503 wg.Add(1) 504 go func(service *docker.Container) { 505 s.waitForServiceContainer(service, time.Duration(waitForServicesTimeout)*time.Second) 506 wg.Done() 507 }(service) 508 } 509 wg.Wait() 510 } 511 } 512 513 func (s *executor) buildServiceLinks(linksMap map[string]*docker.Container) (links []string) { 514 for linkName, container := range linksMap { 515 newContainer, err := s.client.InspectContainer(container.ID) 516 if err != nil { 517 continue 518 } 519 if newContainer.State.Running { 520 links = append(links, container.ID+":"+linkName) 521 } 522 } 523 return 524 } 525 526 func (s *executor) createFromServiceDescription(description string, linksMap map[string]*docker.Container) (err error) { 527 var container *docker.Container 528 529 service, version, linkNames := s.splitServiceAndVersion(description) 530 531 for _, linkName := range linkNames { 532 if linksMap[linkName] != nil { 533 s.Warningln("Service", description, "is already created. Ignoring.") 534 continue 535 } 536 537 // Create service if not yet created 538 if container == nil { 539 container, err = s.createService(service, version) 540 if err != nil { 541 return 542 } 543 s.Debugln("Created service", description, "as", container.ID) 544 s.services = append(s.services, container) 545 } 546 linksMap[linkName] = container 547 } 548 return 549 } 550 551 func (s *executor) createServices() ([]string, error) { 552 serviceNames, err := s.getServiceNames() 553 if err != nil { 554 return nil, err 555 } 556 557 linksMap := make(map[string]*docker.Container) 558 559 for _, serviceDescription := range serviceNames { 560 err = s.createFromServiceDescription(serviceDescription, linksMap) 561 if err != nil { 562 return nil, err 563 } 564 } 565 566 s.waitForServices() 567 568 links := s.buildServiceLinks(linksMap) 569 return links, nil 570 } 571 572 func (s *executor) prepareBuildContainer() (options *docker.CreateContainerOptions, err error) { 573 options = &docker.CreateContainerOptions{ 574 Config: &docker.Config{ 575 Tty: false, 576 AttachStdin: true, 577 AttachStdout: true, 578 AttachStderr: true, 579 OpenStdin: true, 580 StdinOnce: true, 581 Env: append(s.Build.GetAllVariables().StringList(), s.BuildScript.Environment...), 582 }, 583 HostConfig: &docker.HostConfig{ 584 CPUSetCPUs: s.Config.Docker.CPUSetCPUs, 585 DNS: s.Config.Docker.DNS, 586 DNSSearch: s.Config.Docker.DNSSearch, 587 Privileged: s.Config.Docker.Privileged, 588 CapAdd: s.Config.Docker.CapAdd, 589 CapDrop: s.Config.Docker.CapDrop, 590 RestartPolicy: docker.NeverRestart(), 591 ExtraHosts: s.Config.Docker.ExtraHosts, 592 NetworkMode: s.Config.Docker.NetworkMode, 593 Links: s.Config.Docker.Links, 594 LogConfig: docker.LogConfig{ 595 Type: "json-file", 596 }, 597 }, 598 } 599 600 devices, err := s.createDevices() 601 if err != nil { 602 return options, err 603 } 604 options.HostConfig.Devices = devices 605 606 s.Debugln("Creating services...") 607 links, err := s.createServices() 608 if err != nil { 609 return options, err 610 } 611 options.HostConfig.Links = append(options.HostConfig.Links, links...) 612 613 s.Debugln("Creating cache directories...") 614 binds, volumesFrom, err := s.createVolumes() 615 if err != nil { 616 return options, err 617 } 618 options.HostConfig.Binds = binds 619 options.HostConfig.VolumesFrom = volumesFrom 620 return 621 } 622 623 func (s *executor) createContainer(containerType, imageName string, cmd []string, options docker.CreateContainerOptions) (container *docker.Container, err error) { 624 // Fetch image 625 image, err := s.getDockerImage(imageName) 626 if err != nil { 627 return nil, err 628 } 629 630 hostname := s.Config.Docker.Hostname 631 if hostname == "" { 632 hostname = s.Build.ProjectUniqueName() 633 } 634 635 containerName := s.Build.ProjectUniqueName() + "-" + containerType 636 637 // Fill container options 638 options.Name = containerName 639 options.Config.Image = image.ID 640 options.Config.Hostname = hostname 641 options.Config.Cmd = cmd 642 options.Config.Labels = s.getLabels(containerType) 643 644 // this will fail potentially some builds if there's name collision 645 s.removeContainer(containerName) 646 647 s.Debugln("Creating container", options.Name, "...") 648 container, err = s.client.CreateContainer(options) 649 if err != nil { 650 if container != nil { 651 go s.removeContainer(container.ID) 652 } 653 return nil, err 654 } 655 656 s.builds = append(s.builds, container) 657 return 658 } 659 660 func (s *executor) watchContainer(container *docker.Container, input io.Reader, abort chan interface{}) (err error) { 661 s.Debugln("Starting container", container.ID, "...") 662 err = s.client.StartContainer(container.ID, nil) 663 if err != nil { 664 return 665 } 666 667 options := docker.AttachToContainerOptions{ 668 Container: container.ID, 669 InputStream: input, 670 OutputStream: s.BuildLog, 671 ErrorStream: s.BuildLog, 672 Logs: false, 673 Stream: true, 674 Stdin: true, 675 Stdout: true, 676 Stderr: true, 677 RawTerminal: false, 678 } 679 680 waitCh := make(chan error) 681 go func() { 682 s.Debugln("Attaching to container", container.ID, "...") 683 err = s.client.AttachToContainer(options) 684 if err != nil { 685 waitCh <- err 686 return 687 } 688 689 s.Debugln("Waiting for container", container.ID, "...") 690 exitCode, err := s.client.WaitContainer(container.ID) 691 if err == nil { 692 if exitCode != 0 { 693 err = fmt.Errorf("exit code %d", exitCode) 694 } 695 } 696 waitCh <- err 697 }() 698 699 select { 700 case <-abort: 701 s.Debugln("Killing container", container.ID, "...") 702 s.client.KillContainer(docker.KillContainerOptions{ 703 ID: container.ID, 704 }) 705 err = errors.New("Aborted") 706 707 case err = <-waitCh: 708 s.Debugln("Container", container.ID, "finished with", err) 709 } 710 return 711 } 712 713 func (s *executor) removeContainer(id string) error { 714 removeContainerOptions := docker.RemoveContainerOptions{ 715 ID: id, 716 RemoveVolumes: true, 717 Force: true, 718 } 719 err := s.client.RemoveContainer(removeContainerOptions) 720 s.Debugln("Removed container", id, "with", err) 721 return err 722 } 723 724 func (s *executor) verifyAllowedImage(image, optionName string, allowedImages []string, internalImages []string) error { 725 for _, allowedImage := range allowedImages { 726 ok, _ := filepath.Match(allowedImage, image) 727 if ok { 728 return nil 729 } 730 } 731 732 for _, internalImage := range internalImages { 733 if internalImage == image { 734 return nil 735 } 736 } 737 738 if len(allowedImages) != 0 { 739 s.Println() 740 s.Errorln("The", image, "is not present on list of allowed", optionName) 741 for _, allowedImage := range allowedImages { 742 s.Println("-", allowedImage) 743 } 744 s.Println() 745 } else { 746 // by default allow to override the image name 747 return nil 748 } 749 750 s.Println("Please check runner's configuration: http://doc.gitlab.com/ci/docker/using_docker_images.html#overwrite-image-and-services") 751 return errors.New("invalid image") 752 } 753 754 func (s *executor) getImageName() (string, error) { 755 if s.options.Image != "" { 756 image := s.Build.GetAllVariables().ExpandValue(s.options.Image) 757 err := s.verifyAllowedImage(s.options.Image, "images", s.Config.Docker.AllowedImages, []string{s.Config.Docker.Image}) 758 if err != nil { 759 return "", err 760 } 761 return image, nil 762 } 763 764 if s.Config.Docker.Image == "" { 765 return "", errors.New("Missing image") 766 } 767 768 return s.Config.Docker.Image, nil 769 } 770 771 func (s *executor) Prepare(globalConfig *common.Config, config *common.RunnerConfig, build *common.Build) error { 772 err := s.AbstractExecutor.Prepare(globalConfig, config, build) 773 if err != nil { 774 return err 775 } 776 777 if s.BuildScript.PassFile { 778 return errors.New("Docker doesn't support shells that require script file") 779 } 780 781 if config.Docker == nil { 782 return errors.New("Missing docker configuration") 783 } 784 785 err = build.Options.Decode(&s.options) 786 if err != nil { 787 return err 788 } 789 790 imageName, err := s.getImageName() 791 if err != nil { 792 return err 793 } 794 795 s.Println("Using Docker executor with image", imageName, "...") 796 797 client, err := docker_helpers.New(s.Config.Docker.DockerCredentials, dockerAPIVersion) 798 if err != nil { 799 return err 800 } 801 s.client = client 802 return nil 803 } 804 805 func (s *executor) Cleanup() { 806 var wg sync.WaitGroup 807 808 remove := func(id string) { 809 wg.Add(1) 810 go func() { 811 s.removeContainer(id) 812 wg.Done() 813 }() 814 } 815 816 for _, service := range s.services { 817 remove(service.ID) 818 } 819 820 for _, cache := range s.caches { 821 remove(cache.ID) 822 } 823 824 for _, build := range s.builds { 825 remove(build.ID) 826 } 827 828 wg.Wait() 829 830 if s.client != nil { 831 docker_helpers.Close(s.client) 832 } 833 834 s.AbstractExecutor.Cleanup() 835 } 836 837 func (s *executor) runServiceHealthCheckContainer(container *docker.Container, timeout time.Duration) error { 838 waitImage, err := s.getPrebuiltImage("service") 839 if err != nil { 840 return err 841 } 842 843 waitContainerOpts := docker.CreateContainerOptions{ 844 Name: container.Name + "-wait-for-service", 845 Config: &docker.Config{ 846 Image: waitImage.ID, 847 Labels: s.getLabels("wait", "wait="+container.ID), 848 }, 849 HostConfig: &docker.HostConfig{ 850 RestartPolicy: docker.NeverRestart(), 851 Links: []string{container.Name + ":" + container.Name}, 852 NetworkMode: s.Config.Docker.NetworkMode, 853 LogConfig: docker.LogConfig{ 854 Type: "json-file", 855 }, 856 }, 857 } 858 s.Debugln("Waiting for service container", container.Name, "to be up and running...") 859 waitContainer, err := s.client.CreateContainer(waitContainerOpts) 860 if err != nil { 861 return err 862 } 863 defer s.removeContainer(waitContainer.ID) 864 err = s.client.StartContainer(waitContainer.ID, nil) 865 if err != nil { 866 return err 867 } 868 869 waitResult := make(chan error, 1) 870 go func() { 871 statusCode, err := s.client.WaitContainer(waitContainer.ID) 872 if err == nil && statusCode != 0 { 873 err = fmt.Errorf("Status code: %d", statusCode) 874 } 875 waitResult <- err 876 }() 877 878 // these are warnings and they don't make the build fail 879 select { 880 case err := <-waitResult: 881 return err 882 case <-time.After(timeout): 883 return fmt.Errorf("service %v did timeout", container.Name) 884 } 885 } 886 887 func (s *executor) waitForServiceContainer(container *docker.Container, timeout time.Duration) error { 888 err := s.runServiceHealthCheckContainer(container, timeout) 889 if err == nil { 890 return nil 891 } 892 893 var buffer bytes.Buffer 894 buffer.WriteString("\n") 895 buffer.WriteString(helpers.ANSI_YELLOW + "*** WARNING:" + helpers.ANSI_RESET + " Service " + container.Name + " probably didn't start properly.\n") 896 buffer.WriteString("\n") 897 buffer.WriteString(strings.TrimSpace(err.Error()) + "\n") 898 899 var containerBuffer bytes.Buffer 900 901 err = s.client.Logs(docker.LogsOptions{ 902 Container: container.ID, 903 OutputStream: &containerBuffer, 904 ErrorStream: &containerBuffer, 905 Stdout: true, 906 Stderr: true, 907 Timestamps: true, 908 }) 909 if err == nil { 910 if containerLog := containerBuffer.String(); containerLog != "" { 911 buffer.WriteString("\n") 912 buffer.WriteString(strings.TrimSpace(containerLog)) 913 buffer.WriteString("\n") 914 } 915 } else { 916 buffer.WriteString(strings.TrimSpace(err.Error()) + "\n") 917 } 918 919 buffer.WriteString("\n") 920 buffer.WriteString(helpers.ANSI_YELLOW + "*********" + helpers.ANSI_RESET + "\n") 921 buffer.WriteString("\n") 922 io.Copy(s.BuildLog, &buffer) 923 return err 924 }