mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-20 11:48:24 -06:00
If any of the entries in `commands` on `docker_container` resources was empty, the assertion to string panic'd. Since we can't use ValidateFunc on list elements, we can only really check this at apply time. If any value is nil (resolves to empty string during conversion), we fail with an error prior to creating the container. Fixes #6409.
399 lines
10 KiB
Go
399 lines
10 KiB
Go
package docker
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
dc "github.com/fsouza/go-dockerclient"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
)
|
|
|
|
var (
|
|
creationTime time.Time
|
|
)
|
|
|
|
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
|
|
var err error
|
|
client := meta.(*dc.Client)
|
|
|
|
var data Data
|
|
if err := fetchLocalImages(&data, client); err != nil {
|
|
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)
|
|
}
|
|
image = image + ":latest"
|
|
}
|
|
|
|
// 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{}))
|
|
for _, v := range createOpts.Config.Cmd {
|
|
if v == "" {
|
|
return fmt.Errorf("values for command may not be empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
if v, ok := d.GetOk("entrypoint"); ok {
|
|
createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{}))
|
|
}
|
|
|
|
if v, ok := d.GetOk("user"); ok {
|
|
createOpts.Config.User = v.(string)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
extraHosts := []string{}
|
|
if v, ok := d.GetOk("host"); ok {
|
|
extraHosts = extraHostsSetToDockerExtraHosts(v.(*schema.Set))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if v, ok := d.GetOk("labels"); ok {
|
|
createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{}))
|
|
}
|
|
|
|
hostConfig := &dc.HostConfig{
|
|
Privileged: d.Get("privileged").(bool),
|
|
PublishAllPorts: d.Get("publish_all_ports").(bool),
|
|
RestartPolicy: dc.RestartPolicy{
|
|
Name: d.Get("restart").(string),
|
|
MaximumRetryCount: d.Get("max_retry_count").(int),
|
|
},
|
|
LogConfig: dc.LogConfig{
|
|
Type: d.Get("log_driver").(string),
|
|
},
|
|
}
|
|
|
|
if len(portBindings) != 0 {
|
|
hostConfig.PortBindings = portBindings
|
|
}
|
|
if len(extraHosts) != 0 {
|
|
hostConfig.ExtraHosts = extraHosts
|
|
}
|
|
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))
|
|
}
|
|
|
|
if v, ok := d.GetOk("links"); ok {
|
|
hostConfig.Links = stringSetToStringSlice(v.(*schema.Set))
|
|
}
|
|
|
|
if v, ok := d.GetOk("memory"); ok {
|
|
hostConfig.Memory = int64(v.(int)) * 1024 * 1024
|
|
}
|
|
|
|
if v, ok := d.GetOk("memory_swap"); ok {
|
|
swap := int64(v.(int))
|
|
if swap > 0 {
|
|
swap = swap * 1024 * 1024
|
|
}
|
|
hostConfig.MemorySwap = swap
|
|
}
|
|
|
|
if v, ok := d.GetOk("cpu_shares"); ok {
|
|
hostConfig.CPUShares = int64(v.(int))
|
|
}
|
|
|
|
if v, ok := d.GetOk("log_opts"); ok {
|
|
hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{}))
|
|
}
|
|
|
|
if v, ok := d.GetOk("network_mode"); ok {
|
|
hostConfig.NetworkMode = v.(string)
|
|
}
|
|
|
|
createOpts.HostConfig = hostConfig
|
|
|
|
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)
|
|
|
|
if v, ok := d.GetOk("networks"); ok {
|
|
connectionOpts := dc.NetworkConnectionOptions{Container: retContainer.ID}
|
|
|
|
for _, rawNetwork := range v.(*schema.Set).List() {
|
|
network := rawNetwork.(string)
|
|
if err := client.ConnectNetwork(network, connectionOpts); err != nil {
|
|
return fmt.Errorf("Unable to connect to network '%s': %s", network, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
creationTime = time.Now()
|
|
if err := client.StartContainer(retContainer.ID, nil); err != nil {
|
|
return fmt.Errorf("Unable to start container: %s", err)
|
|
}
|
|
|
|
return resourceDockerContainerRead(d, meta)
|
|
}
|
|
|
|
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*dc.Client)
|
|
|
|
apiContainer, err := fetchDockerContainer(d.Id(), client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if apiContainer == nil {
|
|
// This container doesn't exist anymore
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
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 ||
|
|
!container.State.Running && !d.Get("must_run").(bool) {
|
|
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
return nil
|
|
}
|
|
|
|
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
|
|
client := meta.(*dc.Client)
|
|
|
|
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 {
|
|
if v == nil {
|
|
ret = append(ret, "")
|
|
continue
|
|
}
|
|
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
|
|
}
|
|
|
|
func mapTypeMapValsToString(typeMap map[string]interface{}) map[string]string {
|
|
mapped := make(map[string]string, len(typeMap))
|
|
for k, v := range typeMap {
|
|
mapped[k] = v.(string)
|
|
}
|
|
return mapped
|
|
}
|
|
|
|
func fetchDockerContainer(ID 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 {
|
|
if apiContainer.ID == ID {
|
|
return &apiContainer, nil
|
|
}
|
|
}
|
|
|
|
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 extraHostsSetToDockerExtraHosts(extraHosts *schema.Set) []string {
|
|
retExtraHosts := []string{}
|
|
|
|
for _, hostInt := range extraHosts.List() {
|
|
host := hostInt.(map[string]interface{})
|
|
ip := host["ip"].(string)
|
|
hostname := host["host"].(string)
|
|
retExtraHosts = append(retExtraHosts, hostname+":"+ip)
|
|
}
|
|
|
|
return retExtraHosts
|
|
}
|
|
|
|
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)
|
|
volumeName := volume["volume_name"].(string)
|
|
if len(volumeName) == 0 {
|
|
volumeName = 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(volumeName) != 0:
|
|
readWrite := "rw"
|
|
if readOnly {
|
|
readWrite = "ro"
|
|
}
|
|
retVolumeMap[containerPath] = struct{}{}
|
|
retHostConfigBinds = append(retHostConfigBinds, volumeName+":"+containerPath+":"+readWrite)
|
|
default:
|
|
retVolumeMap[containerPath] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, nil
|
|
}
|