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  }