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 }