github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/cloud/providers/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"math/rand"
     7  	"time"
     8  
     9  	"github.com/evergreen-ci/evergreen"
    10  	"github.com/evergreen-ci/evergreen/cloud"
    11  	"github.com/evergreen-ci/evergreen/db/bsonutil"
    12  	"github.com/evergreen-ci/evergreen/hostutil"
    13  	"github.com/evergreen-ci/evergreen/model/distro"
    14  	"github.com/evergreen-ci/evergreen/model/host"
    15  	docker "github.com/fsouza/go-dockerclient"
    16  	"github.com/mitchellh/mapstructure"
    17  	"github.com/mongodb/grip"
    18  	"github.com/pkg/errors"
    19  	"gopkg.in/mgo.v2/bson"
    20  )
    21  
    22  const (
    23  	DockerStatusRunning = iota
    24  	DockerStatusPaused
    25  	DockerStatusRestarting
    26  	DockerStatusKilled
    27  	DockerStatusUnknown
    28  
    29  	ProviderName   = "docker"
    30  	TimeoutSeconds = 5
    31  )
    32  
    33  type DockerManager struct {
    34  }
    35  
    36  type portRange struct {
    37  	MinPort int64 `mapstructure:"min_port" json:"min_port" bson:"min_port"`
    38  	MaxPort int64 `mapstructure:"max_port" json:"max_port" bson:"max_port"`
    39  }
    40  
    41  type auth struct {
    42  	Cert string `mapstructure:"cert" json:"cert" bson:"cert"`
    43  	Key  string `mapstructure:"key" json:"key" bson:"key"`
    44  	Ca   string `mapstructure:"ca" json:"ca" bson:"ca"`
    45  }
    46  
    47  type Settings struct {
    48  	HostIp     string     `mapstructure:"host_ip" json:"host_ip" bson:"host_ip"`
    49  	BindIp     string     `mapstructure:"bind_ip" json:"bind_ip" bson:"bind_ip"`
    50  	ImageId    string     `mapstructure:"image_name" json:"image_name" bson:"image_name"`
    51  	ClientPort int        `mapstructure:"client_port" json:"client_port" bson:"client_port"`
    52  	PortRange  *portRange `mapstructure:"port_range" json:"port_range" bson:"port_range"`
    53  	Auth       *auth      `mapstructure:"auth" json:"auth" bson:"auth"`
    54  }
    55  
    56  var (
    57  	// bson fields for the Settings struct
    58  	HostIp     = bsonutil.MustHaveTag(Settings{}, "HostIp")
    59  	BindIp     = bsonutil.MustHaveTag(Settings{}, "BindIp")
    60  	ImageId    = bsonutil.MustHaveTag(Settings{}, "ImageId")
    61  	ClientPort = bsonutil.MustHaveTag(Settings{}, "ClientPort")
    62  	PortRange  = bsonutil.MustHaveTag(Settings{}, "PortRange")
    63  	Auth       = bsonutil.MustHaveTag(Settings{}, "Auth")
    64  
    65  	// bson fields for the portRange struct
    66  	MinPort = bsonutil.MustHaveTag(portRange{}, "MinPort")
    67  	MaxPort = bsonutil.MustHaveTag(portRange{}, "MaxPort")
    68  
    69  	// bson fields for the auth struct
    70  	Cert = bsonutil.MustHaveTag(auth{}, "Cert")
    71  	Key  = bsonutil.MustHaveTag(auth{}, "Key")
    72  	Ca   = bsonutil.MustHaveTag(auth{}, "Ca")
    73  
    74  	// exposed port (set to 22/tcp, default ssh port)
    75  	SSHDPort docker.Port = "22/tcp"
    76  )
    77  
    78  //*********************************************************************************
    79  // Helper Functions
    80  //*********************************************************************************
    81  
    82  func generateClient(d *distro.Distro) (*docker.Client, *Settings, error) {
    83  	// Populate and validate settings
    84  	settings := &Settings{} // Instantiate global settings
    85  	if err := mapstructure.Decode(d.ProviderSettings, settings); err != nil {
    86  		return nil, settings, errors.Wrapf(err, "Error decoding params for distro %v", d.Id)
    87  	}
    88  
    89  	if err := settings.Validate(); err != nil {
    90  		return nil, settings, errors.Wrapf(err, "Invalid Docker settings in distro %v: %v", d.Id)
    91  	}
    92  
    93  	// Convert authentication strings to byte arrays
    94  	cert := bytes.NewBufferString(settings.Auth.Cert).Bytes()
    95  	key := bytes.NewBufferString(settings.Auth.Key).Bytes()
    96  	ca := bytes.NewBufferString(settings.Auth.Ca).Bytes()
    97  
    98  	// Create client
    99  	endpoint := fmt.Sprintf("tcp://%s:%v", settings.HostIp, settings.ClientPort)
   100  	client, err := docker.NewTLSClientFromBytes(endpoint, cert, key, ca)
   101  
   102  	err = errors.Wrapf(err, "Docker initialize client API call failed for host '%s'", endpoint)
   103  	grip.Error(err)
   104  
   105  	return client, settings, err
   106  }
   107  
   108  func populateHostConfig(hostConfig *docker.HostConfig, d *distro.Distro) error {
   109  	// Retrieve client for API call and settings
   110  	client, settings, err := generateClient(d)
   111  	if err != nil {
   112  		return errors.WithStack(err)
   113  	}
   114  	minPort := settings.PortRange.MinPort
   115  	maxPort := settings.PortRange.MaxPort
   116  
   117  	// Get all the things!
   118  	containers, err := client.ListContainers(docker.ListContainersOptions{})
   119  	err = errors.Wrap(err, "Docker list containers API call failed.")
   120  	if err != nil {
   121  		grip.Error(err)
   122  		return err
   123  	}
   124  
   125  	reservedPorts := make(map[int64]bool)
   126  	for _, c := range containers {
   127  		for _, p := range c.Ports {
   128  			reservedPorts[p.PublicPort] = true
   129  		}
   130  	}
   131  
   132  	// If unspecified, let Docker choose random port
   133  	if minPort == 0 && maxPort == 0 {
   134  		hostConfig.PublishAllPorts = true
   135  		return nil
   136  	}
   137  
   138  	hostConfig.PortBindings = make(map[docker.Port][]docker.PortBinding)
   139  	for i := minPort; i <= maxPort; i++ {
   140  		// if port is not already in use, bind it to sshd exposed container port
   141  		if !reservedPorts[i] {
   142  			hostConfig.PortBindings[SSHDPort] = []docker.PortBinding{
   143  				{
   144  					HostIP:   settings.BindIp,
   145  					HostPort: fmt.Sprintf("%v", i),
   146  				},
   147  			}
   148  			break
   149  		}
   150  	}
   151  
   152  	// If map is empty, no ports were available.
   153  	if len(hostConfig.PortBindings) == 0 {
   154  		err := errors.New("No available ports in specified range")
   155  		grip.Error(err)
   156  		return err
   157  	}
   158  
   159  	return nil
   160  }
   161  
   162  func retrieveOpenPortBinding(containerPtr *docker.Container) (string, error) {
   163  	exposedPorts := containerPtr.Config.ExposedPorts
   164  	ports := containerPtr.NetworkSettings.Ports
   165  	for k := range exposedPorts {
   166  		portBindings := ports[k]
   167  		if len(portBindings) > 0 {
   168  			return portBindings[0].HostPort, nil
   169  		}
   170  	}
   171  	return "", errors.New("No available ports")
   172  }
   173  
   174  //*********************************************************************************
   175  // Public Functions
   176  //*********************************************************************************
   177  
   178  //Validate checks that the settings from the config file are sane.
   179  func (settings *Settings) Validate() error {
   180  	if settings.HostIp == "" {
   181  		return errors.New("HostIp must not be blank")
   182  	}
   183  
   184  	if settings.ImageId == "" {
   185  		return errors.New("ImageName must not be blank")
   186  	}
   187  
   188  	if settings.ClientPort == 0 {
   189  		return errors.New("Port must not be blank")
   190  	}
   191  
   192  	if settings.PortRange != nil {
   193  		min := settings.PortRange.MinPort
   194  		max := settings.PortRange.MaxPort
   195  
   196  		if max < min {
   197  			return errors.New("Container port range must be valid")
   198  		}
   199  	}
   200  
   201  	if settings.Auth == nil {
   202  		return errors.New("Authentication materials must not be blank")
   203  	} else if settings.Auth.Cert == "" {
   204  		return errors.New("Certificate must not be blank")
   205  	} else if settings.Auth.Key == "" {
   206  		return errors.New("Key must not be blank")
   207  	} else if settings.Auth.Ca == "" {
   208  		return errors.New("Certificate authority must not be blank")
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (_ *DockerManager) GetSettings() cloud.ProviderSettings {
   215  	return &Settings{}
   216  }
   217  
   218  // SpawnInstance creates and starts a new Docker container
   219  func (dockerMgr *DockerManager) SpawnInstance(d *distro.Distro, hostOpts cloud.HostOptions) (*host.Host, error) {
   220  	var err error
   221  
   222  	if d.Provider != ProviderName {
   223  		return nil, errors.Errorf("Can't spawn instance of %v for distro %v: provider is %v", ProviderName, d.Id, d.Provider)
   224  	}
   225  
   226  	// Initialize client
   227  	dockerClient, settings, err := generateClient(d)
   228  	if err != nil {
   229  		return nil, errors.WithStack(err)
   230  	}
   231  
   232  	// Create HostConfig structure
   233  	hostConfig := &docker.HostConfig{}
   234  	err = populateHostConfig(hostConfig, d)
   235  	if err != nil {
   236  		err = errors.Wrapf(err, "Unable to populate docker host config for host '%s'", settings.HostIp)
   237  		grip.Error(err)
   238  		return nil, err
   239  	}
   240  
   241  	// Build container
   242  	containerName := "docker-" + bson.NewObjectId().Hex()
   243  	newContainer, err := dockerClient.CreateContainer(
   244  		docker.CreateContainerOptions{
   245  			Name: containerName,
   246  			Config: &docker.Config{
   247  				Cmd: []string{"/usr/sbin/sshd", "-D"},
   248  				ExposedPorts: map[docker.Port]struct{}{
   249  					SSHDPort: {},
   250  				},
   251  				Image: settings.ImageId,
   252  			},
   253  			HostConfig: hostConfig,
   254  		},
   255  	)
   256  	if err != nil {
   257  		err = errors.Wrapf(err, "Docker create container API call failed for host '%s'", settings.HostIp)
   258  		grip.Error(err)
   259  		return nil, err
   260  	}
   261  
   262  	// Start container
   263  	err = dockerClient.StartContainer(newContainer.ID, nil)
   264  	if err != nil {
   265  		err = errors.Wrapf(err, "Docker start container API call failed for host '%s'", settings.HostIp)
   266  		// Clean up
   267  		err2 := dockerClient.RemoveContainer(
   268  			docker.RemoveContainerOptions{
   269  				ID:    newContainer.ID,
   270  				Force: true,
   271  			},
   272  		)
   273  		if err2 != nil {
   274  			err = errors.Errorf("start container error: %+v;\nunable to cleanup: %+v", err, err2)
   275  		}
   276  		grip.Error(err)
   277  		return nil, err
   278  	}
   279  
   280  	// Retrieve container details
   281  	newContainer, err = dockerClient.InspectContainer(newContainer.ID)
   282  	if err != nil {
   283  		err = errors.Wrapf(err, "Docker inspect container API call failed for host '%s'", settings.HostIp)
   284  		grip.Error(err)
   285  		return nil, err
   286  	}
   287  
   288  	hostPort, err := retrieveOpenPortBinding(newContainer)
   289  	if err != nil {
   290  		grip.Errorf("Error with docker container '%v': %v", newContainer.ID, err)
   291  		return nil, err
   292  	}
   293  
   294  	hostStr := fmt.Sprintf("%s:%s", settings.BindIp, hostPort)
   295  	// Add host info to db
   296  	instanceName := "container-" +
   297  		fmt.Sprintf("%d", rand.New(rand.NewSource(time.Now().UnixNano())).Int())
   298  
   299  	intentHost := cloud.NewIntent(*d, instanceName, ProviderName, hostOpts)
   300  	intentHost.Host = hostStr
   301  
   302  	err = errors.Wrapf(intentHost.Insert(), "failed to insert new host '%s'", intentHost.Id)
   303  	if err != nil {
   304  		grip.Error(err)
   305  		return nil, err
   306  	}
   307  
   308  	grip.Debugf("Successfully inserted new host '%s' for distro '%s'", intentHost.Id, d.Id)
   309  	return intentHost, nil
   310  }
   311  
   312  // getStatus is a helper function which returns the enum representation of the status
   313  // contained in a container's state
   314  func getStatus(s *docker.State) int {
   315  	if s.Running {
   316  		return DockerStatusRunning
   317  	} else if s.Paused {
   318  		return DockerStatusPaused
   319  	} else if s.Restarting {
   320  		return DockerStatusRestarting
   321  	} else if s.OOMKilled {
   322  		return DockerStatusKilled
   323  	}
   324  
   325  	return DockerStatusUnknown
   326  }
   327  
   328  // GetInstanceStatus returns a universal status code representing the state
   329  // of a container.
   330  func (dockerMgr *DockerManager) GetInstanceStatus(host *host.Host) (cloud.CloudStatus, error) {
   331  	dockerClient, _, err := generateClient(&host.Distro)
   332  	if err != nil {
   333  		return cloud.StatusUnknown, err
   334  	}
   335  
   336  	container, err := dockerClient.InspectContainer(host.Id)
   337  	if err != nil {
   338  		return cloud.StatusUnknown, errors.Wrapf(err, "Failed to get container information for host '%v'", host.Id)
   339  	}
   340  
   341  	switch getStatus(&container.State) {
   342  	case DockerStatusRestarting:
   343  		return cloud.StatusInitializing, nil
   344  	case DockerStatusRunning:
   345  		return cloud.StatusRunning, nil
   346  	case DockerStatusPaused:
   347  		return cloud.StatusStopped, nil
   348  	case DockerStatusKilled:
   349  		return cloud.StatusTerminated, nil
   350  	default:
   351  		return cloud.StatusUnknown, nil
   352  	}
   353  }
   354  
   355  //GetDNSName gets the DNS hostname of a container by reading it directly from
   356  //the Docker API
   357  func (dockerMgr *DockerManager) GetDNSName(host *host.Host) (string, error) {
   358  	return host.Host, nil
   359  }
   360  
   361  //CanSpawn returns if a given cloud provider supports spawning a new host
   362  //dynamically. Always returns true for Docker.
   363  func (dockerMgr *DockerManager) CanSpawn() (bool, error) {
   364  	return true, nil
   365  }
   366  
   367  //TerminateInstance destroys a container.
   368  func (dockerMgr *DockerManager) TerminateInstance(host *host.Host) error {
   369  	dockerClient, _, err := generateClient(&host.Distro)
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	if err != nil {
   375  		err = errors.Wrapf(dockerClient.StopContainer(host.Id, TimeoutSeconds),
   376  			"failed to stop container '%s'", host.Id)
   377  		grip.Error(err)
   378  		return err
   379  	}
   380  
   381  	err = dockerClient.RemoveContainer(
   382  		docker.RemoveContainerOptions{
   383  			ID: host.Id,
   384  		})
   385  
   386  	if err != nil {
   387  		err = errors.Wrapf(err, "Failed to remove container '%s'", host.Id)
   388  		grip.Error(err)
   389  		return err
   390  	}
   391  
   392  	return host.Terminate()
   393  }
   394  
   395  //Configure populates a DockerManager by reading relevant settings from the
   396  //config object.
   397  func (dockerMgr *DockerManager) Configure(settings *evergreen.Settings) error {
   398  	return nil
   399  }
   400  
   401  //IsSSHReachable checks if a container appears to be reachable via SSH by
   402  //attempting to contact the host directly.
   403  func (dockerMgr *DockerManager) IsSSHReachable(host *host.Host, keyPath string) (bool, error) {
   404  	sshOpts, err := dockerMgr.GetSSHOptions(host, keyPath)
   405  	if err != nil {
   406  		return false, err
   407  	}
   408  	return hostutil.CheckSSHResponse(host, sshOpts)
   409  }
   410  
   411  //IsUp checks the container's state by querying the Docker API and
   412  //returns true if the host should be available to connect with SSH.
   413  func (dockerMgr *DockerManager) IsUp(host *host.Host) (bool, error) {
   414  	cloudStatus, err := dockerMgr.GetInstanceStatus(host)
   415  	if err != nil {
   416  		return false, err
   417  	}
   418  	if cloudStatus == cloud.StatusRunning {
   419  		return true, nil
   420  	}
   421  	return false, nil
   422  }
   423  
   424  func (dockerMgr *DockerManager) OnUp(host *host.Host) error {
   425  	return nil
   426  }
   427  
   428  //GetSSHOptions returns an array of default SSH options for connecting to a
   429  //container.
   430  func (dockerMgr *DockerManager) GetSSHOptions(host *host.Host, keyPath string) ([]string, error) {
   431  	if keyPath == "" {
   432  		return []string{}, errors.New("No key specified for Docker host")
   433  	}
   434  
   435  	opts := []string{"-i", keyPath}
   436  	for _, opt := range host.Distro.SSHOptions {
   437  		opts = append(opts, "-o", opt)
   438  	}
   439  	return opts, nil
   440  }
   441  
   442  // TimeTilNextPayment returns the amount of time until the next payment is due
   443  // for the host. For Docker this is not relevant.
   444  func (dockerMgr *DockerManager) TimeTilNextPayment(host *host.Host) time.Duration {
   445  	return time.Duration(0)
   446  }