2015-02-17 17:28:33 +01:00
|
|
|
package docker
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2015-06-12 20:44:37 +02:00
|
|
|
"time"
|
2015-02-17 17:28:33 +01:00
|
|
|
|
|
|
|
dc "github.com/fsouza/go-dockerclient"
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
|
|
)
|
|
|
|
|
2015-06-25 16:38:56 +02:00
|
|
|
var (
|
|
|
|
creationTime time.Time
|
|
|
|
)
|
|
|
|
|
2015-02-17 17:28:33 +01:00
|
|
|
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
|
2015-03-29 03:37:20 +02:00
|
|
|
var err error
|
|
|
|
client := meta.(*dc.Client)
|
2015-02-17 17:28:33 +01:00
|
|
|
|
2015-03-29 03:37:20 +02:00
|
|
|
var data Data
|
|
|
|
if err := fetchLocalImages(&data, client); err != nil {
|
2015-02-17 17:28:33 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
image := d.Get("image").(string)
|
|
|
|
if _, ok := data.DockerImages[image]; !ok {
|
|
|
|
if _, ok := data.DockerImages[image+":latest"]; !ok {
|
|
|
|
return fmt.Errorf("Unable to find image %s", image)
|
|
|
|
}
|
2015-06-25 16:38:56 +02:00
|
|
|
image = image + ":latest"
|
2015-02-17 17:28:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// The awesome, wonderful, splendiferous, sensical
|
|
|
|
// Docker API now lets you specify a HostConfig in
|
|
|
|
// CreateContainerOptions, but in my testing it still only
|
|
|
|
// actually applies HostConfig options set in StartContainer.
|
|
|
|
// How cool is that?
|
|
|
|
createOpts := dc.CreateContainerOptions{
|
|
|
|
Name: d.Get("name").(string),
|
|
|
|
Config: &dc.Config{
|
|
|
|
Image: image,
|
|
|
|
Hostname: d.Get("hostname").(string),
|
|
|
|
Domainname: d.Get("domainname").(string),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("env"); ok {
|
|
|
|
createOpts.Config.Env = stringSetToStringSlice(v.(*schema.Set))
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("command"); ok {
|
|
|
|
createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{}))
|
|
|
|
}
|
|
|
|
|
2015-10-26 22:24:48 +01:00
|
|
|
if v, ok := d.GetOk("entrypoint"); ok {
|
|
|
|
createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{}))
|
|
|
|
}
|
|
|
|
|
2015-02-17 17:28:33 +01:00
|
|
|
exposedPorts := map[dc.Port]struct{}{}
|
|
|
|
portBindings := map[dc.Port][]dc.PortBinding{}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("ports"); ok {
|
|
|
|
exposedPorts, portBindings = portSetToDockerPorts(v.(*schema.Set))
|
|
|
|
}
|
|
|
|
if len(exposedPorts) != 0 {
|
|
|
|
createOpts.Config.ExposedPorts = exposedPorts
|
|
|
|
}
|
|
|
|
|
|
|
|
volumes := map[string]struct{}{}
|
|
|
|
binds := []string{}
|
|
|
|
volumesFrom := []string{}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("volumes"); ok {
|
|
|
|
volumes, binds, volumesFrom, err = volumeSetToDockerVolumes(v.(*schema.Set))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Unable to parse volumes: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(volumes) != 0 {
|
|
|
|
createOpts.Config.Volumes = volumes
|
|
|
|
}
|
|
|
|
|
2015-11-03 21:20:58 +01:00
|
|
|
if v, ok := d.GetOk("labels"); ok {
|
|
|
|
createOpts.Config.Labels = mapLabels(v.(map[string]interface{}))
|
|
|
|
}
|
|
|
|
|
2015-02-17 17:28:33 +01:00
|
|
|
var retContainer *dc.Container
|
|
|
|
if retContainer, err = client.CreateContainer(createOpts); err != nil {
|
|
|
|
return fmt.Errorf("Unable to create container: %s", err)
|
|
|
|
}
|
|
|
|
if retContainer == nil {
|
|
|
|
return fmt.Errorf("Returned container is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
d.SetId(retContainer.ID)
|
|
|
|
|
|
|
|
hostConfig := &dc.HostConfig{
|
2015-06-24 07:31:24 +02:00
|
|
|
Privileged: d.Get("privileged").(bool),
|
2015-02-17 17:28:33 +01:00
|
|
|
PublishAllPorts: d.Get("publish_all_ports").(bool),
|
2015-10-27 17:08:57 +01:00
|
|
|
RestartPolicy: dc.RestartPolicy{
|
|
|
|
Name: d.Get("restart").(string),
|
|
|
|
MaximumRetryCount: d.Get("max_retry_count").(int),
|
|
|
|
},
|
2015-02-17 17:28:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(portBindings) != 0 {
|
|
|
|
hostConfig.PortBindings = portBindings
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(binds) != 0 {
|
|
|
|
hostConfig.Binds = binds
|
|
|
|
}
|
|
|
|
if len(volumesFrom) != 0 {
|
|
|
|
hostConfig.VolumesFrom = volumesFrom
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("dns"); ok {
|
|
|
|
hostConfig.DNS = stringSetToStringSlice(v.(*schema.Set))
|
|
|
|
}
|
|
|
|
|
2015-04-16 21:42:21 +02:00
|
|
|
if v, ok := d.GetOk("links"); ok {
|
|
|
|
hostConfig.Links = stringSetToStringSlice(v.(*schema.Set))
|
|
|
|
}
|
|
|
|
|
2015-10-28 00:53:49 +01:00
|
|
|
if v, ok := d.GetOk("memory"); ok {
|
|
|
|
memory := int64(v.(int))
|
|
|
|
if memory > 0 {
|
|
|
|
hostConfig.Memory = memory * 1024 * 1024
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("memory_swap"); ok {
|
|
|
|
swap := int64(v.(int))
|
|
|
|
if swap != 0 {
|
|
|
|
if swap > 0 { // only convert positive #s to bytes
|
|
|
|
swap = swap * 1024 * 1024
|
|
|
|
}
|
|
|
|
hostConfig.MemorySwap = swap
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("cpu_shares"); ok {
|
|
|
|
shares := int64(v.(int))
|
|
|
|
if shares > 0 {
|
|
|
|
hostConfig.CPUShares = shares
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-25 16:38:56 +02:00
|
|
|
creationTime = time.Now()
|
2015-02-17 17:28:33 +01:00
|
|
|
if err := client.StartContainer(retContainer.ID, hostConfig); err != nil {
|
|
|
|
return fmt.Errorf("Unable to start container: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return resourceDockerContainerRead(d, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
|
2015-03-29 03:37:20 +02:00
|
|
|
client := meta.(*dc.Client)
|
2015-02-17 17:28:33 +01:00
|
|
|
|
|
|
|
apiContainer, err := fetchDockerContainer(d.Get("name").(string), client)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if apiContainer == nil {
|
|
|
|
// This container doesn't exist anymore
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-06-25 16:38:56 +02:00
|
|
|
var container *dc.Container
|
|
|
|
|
|
|
|
loops := 1 // if it hasn't just been created, don't delay
|
|
|
|
if !creationTime.IsZero() {
|
|
|
|
loops = 30 // with 500ms spacing, 15 seconds; ought to be plenty
|
|
|
|
}
|
|
|
|
sleepTime := 500 * time.Millisecond
|
|
|
|
|
|
|
|
for i := loops; i > 0; i-- {
|
|
|
|
container, err = client.InspectContainer(apiContainer.ID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error inspecting container %s: %s", apiContainer.ID, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if container.State.Running ||
|
2015-10-08 14:48:04 +02:00
|
|
|
!container.State.Running && !d.Get("must_run").(bool) {
|
2015-06-25 16:38:56 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if creationTime.IsZero() { // We didn't just create it, so don't wait around
|
|
|
|
return resourceDockerContainerDelete(d, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
if container.State.FinishedAt.After(creationTime) {
|
|
|
|
// It exited immediately, so error out so dependent containers
|
|
|
|
// aren't started
|
|
|
|
resourceDockerContainerDelete(d, meta)
|
|
|
|
return fmt.Errorf("Container %s exited after creation, error was: %s", apiContainer.ID, container.State.Error)
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(sleepTime)
|
2015-02-17 17:28:33 +01:00
|
|
|
}
|
|
|
|
|
2015-06-25 16:38:56 +02:00
|
|
|
// Handle the case of the for loop above running its course
|
|
|
|
if !container.State.Running && d.Get("must_run").(bool) {
|
|
|
|
resourceDockerContainerDelete(d, meta)
|
|
|
|
return fmt.Errorf("Container %s failed to be in running state", apiContainer.ID)
|
2015-02-17 17:28:33 +01:00
|
|
|
}
|
|
|
|
|
2015-04-20 19:42:36 +02:00
|
|
|
// Read Network Settings
|
|
|
|
if container.NetworkSettings != nil {
|
|
|
|
d.Set("ip_address", container.NetworkSettings.IPAddress)
|
|
|
|
d.Set("ip_prefix_length", container.NetworkSettings.IPPrefixLen)
|
|
|
|
d.Set("gateway", container.NetworkSettings.Gateway)
|
|
|
|
d.Set("bridge", container.NetworkSettings.Bridge)
|
|
|
|
}
|
2015-04-16 15:21:14 +02:00
|
|
|
|
2015-02-17 17:28:33 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
|
2015-03-29 03:37:20 +02:00
|
|
|
client := meta.(*dc.Client)
|
2015-02-17 17:28:33 +01:00
|
|
|
|
|
|
|
removeOpts := dc.RemoveContainerOptions{
|
|
|
|
ID: d.Id(),
|
|
|
|
RemoveVolumes: true,
|
|
|
|
Force: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := client.RemoveContainer(removeOpts); err != nil {
|
|
|
|
return fmt.Errorf("Error deleting container %s: %s", d.Id(), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func stringListToStringSlice(stringList []interface{}) []string {
|
|
|
|
ret := []string{}
|
|
|
|
for _, v := range stringList {
|
|
|
|
ret = append(ret, v.(string))
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
func stringSetToStringSlice(stringSet *schema.Set) []string {
|
|
|
|
ret := []string{}
|
|
|
|
if stringSet == nil {
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
for _, envVal := range stringSet.List() {
|
|
|
|
ret = append(ret, envVal.(string))
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2015-11-03 21:20:58 +01:00
|
|
|
func mapLabels(labels map[string]interface{}) map[string]string {
|
|
|
|
mapped := make(map[string]string, len(labels))
|
|
|
|
for k, v := range labels {
|
|
|
|
mapped[k] = v.(string)
|
|
|
|
}
|
|
|
|
return mapped
|
|
|
|
}
|
|
|
|
|
2015-02-17 17:28:33 +01:00
|
|
|
func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) {
|
|
|
|
apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error fetching container information from Docker: %s\n", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, apiContainer := range apiContainers {
|
|
|
|
// Sometimes the Docker API prefixes container names with /
|
|
|
|
// like it does in these commands. But if there's no
|
|
|
|
// set name, it just uses the ID without a /...ugh.
|
2015-06-12 23:01:36 +02:00
|
|
|
switch len(apiContainer.Names) {
|
|
|
|
case 0:
|
|
|
|
if apiContainer.ID == name {
|
|
|
|
return &apiContainer, nil
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
for _, containerName := range apiContainer.Names {
|
|
|
|
if strings.TrimLeft(containerName, "/") == name {
|
|
|
|
return &apiContainer, nil
|
|
|
|
}
|
|
|
|
}
|
2015-02-17 17:28:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func portSetToDockerPorts(ports *schema.Set) (map[dc.Port]struct{}, map[dc.Port][]dc.PortBinding) {
|
|
|
|
retExposedPorts := map[dc.Port]struct{}{}
|
|
|
|
retPortBindings := map[dc.Port][]dc.PortBinding{}
|
|
|
|
|
|
|
|
for _, portInt := range ports.List() {
|
|
|
|
port := portInt.(map[string]interface{})
|
|
|
|
internal := port["internal"].(int)
|
|
|
|
protocol := port["protocol"].(string)
|
|
|
|
|
|
|
|
exposedPort := dc.Port(strconv.Itoa(internal) + "/" + protocol)
|
|
|
|
retExposedPorts[exposedPort] = struct{}{}
|
|
|
|
|
|
|
|
external, extOk := port["external"].(int)
|
|
|
|
ip, ipOk := port["ip"].(string)
|
|
|
|
|
|
|
|
if extOk {
|
|
|
|
portBinding := dc.PortBinding{
|
|
|
|
HostPort: strconv.Itoa(external),
|
|
|
|
}
|
|
|
|
if ipOk {
|
|
|
|
portBinding.HostIP = ip
|
|
|
|
}
|
|
|
|
retPortBindings[exposedPort] = append(retPortBindings[exposedPort], portBinding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return retExposedPorts, retPortBindings
|
|
|
|
}
|
|
|
|
|
|
|
|
func volumeSetToDockerVolumes(volumes *schema.Set) (map[string]struct{}, []string, []string, error) {
|
|
|
|
retVolumeMap := map[string]struct{}{}
|
|
|
|
retHostConfigBinds := []string{}
|
|
|
|
retVolumeFromContainers := []string{}
|
|
|
|
|
|
|
|
for _, volumeInt := range volumes.List() {
|
|
|
|
volume := volumeInt.(map[string]interface{})
|
|
|
|
fromContainer := volume["from_container"].(string)
|
|
|
|
containerPath := volume["container_path"].(string)
|
|
|
|
hostPath := volume["host_path"].(string)
|
|
|
|
readOnly := volume["read_only"].(bool)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case len(fromContainer) == 0 && len(containerPath) == 0:
|
|
|
|
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Volume entry without container path or source container")
|
|
|
|
case len(fromContainer) != 0 && len(containerPath) != 0:
|
|
|
|
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Both a container and a path specified in a volume entry")
|
|
|
|
case len(fromContainer) != 0:
|
|
|
|
retVolumeFromContainers = append(retVolumeFromContainers, fromContainer)
|
|
|
|
case len(hostPath) != 0:
|
|
|
|
readWrite := "rw"
|
|
|
|
if readOnly {
|
|
|
|
readWrite = "ro"
|
|
|
|
}
|
|
|
|
retVolumeMap[containerPath] = struct{}{}
|
|
|
|
retHostConfigBinds = append(retHostConfigBinds, hostPath+":"+containerPath+":"+readWrite)
|
|
|
|
default:
|
|
|
|
retVolumeMap[containerPath] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, nil
|
|
|
|
}
|