mirror of
https://github.com/kreuzwerker/terraform-provider-docker.git
synced 2025-12-20 22:59:42 -05:00
Initial commit. This adds the initial bits of a Docker provider.
Docker's API is huge and only a small subset is currently implemented, but this is expected to grow over time. Currently it's enough to satisfy the use cases of probably 95% of Docker users. I'm preparing this initial pull request as a preview step for feedback. My ideal scenario would be to develop this within a branch in the main repository; the more eyes and testing and pitching in on the code, the better (this would avoid a merge request-to-the-merge-request scenario, as I figure this will be built up over the longer term, even before a merge into master). Unit tests do not exist yet. Right now I've just been focused on getting initial functionality ported over. I've been testing each option extensively via the Docker inspect capabilities. This code (C)2014-2015 Akamai Technologies, Inc. <opensource@akamai.com>
This commit is contained in:
commit
77e1e6067e
6 changed files with 770 additions and 0 deletions
24
config.go
Normal file
24
config.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import dc "github.com/fsouza/go-dockerclient"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DockerHost string
|
||||||
|
SkipPull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
DockerImages map[string]*dc.APIImages
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient() returns a new Docker client.
|
||||||
|
func (c *Config) NewClient() (*dc.Client, error) {
|
||||||
|
return dc.NewClient(c.DockerHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewData() returns a new data struct.
|
||||||
|
func (c *Config) NewData() *Data {
|
||||||
|
return &Data{
|
||||||
|
DockerImages: map[string]*dc.APIImages{},
|
||||||
|
}
|
||||||
|
}
|
||||||
34
provider.go
Normal file
34
provider.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Provider() terraform.ResourceProvider {
|
||||||
|
return &schema.Provider{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"docker_host": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:/run/docker.sock"),
|
||||||
|
Description: "The Docker daemon endpoint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
|
"docker_container": resourceDockerContainer(),
|
||||||
|
"docker_image": resourceDockerImage(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ConfigureFunc: providerConfigure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||||
|
config := Config{
|
||||||
|
DockerHost: d.Get("docker_host").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
222
resource_docker_container.go
Normal file
222
resource_docker_container.go
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceDockerContainer() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceDockerContainerCreate,
|
||||||
|
Read: resourceDockerContainerRead,
|
||||||
|
Update: resourceDockerContainerUpdate,
|
||||||
|
Delete: resourceDockerContainerDelete,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Indicates whether the container must be running.
|
||||||
|
//
|
||||||
|
// An assumption is made that configured containers
|
||||||
|
// should be running; if not, they should not be in
|
||||||
|
// the configuration. Therefore a stopped container
|
||||||
|
// should be started. Set to false to have the
|
||||||
|
// provider leave the container alone.
|
||||||
|
//
|
||||||
|
// Actively-debugged containers are likely to be
|
||||||
|
// stopped and started manually, and Docker has
|
||||||
|
// some provisions for restarting containers that
|
||||||
|
// stop. The utility here comes from the fact that
|
||||||
|
// this will delete and re-create the container
|
||||||
|
// following the principle that the containers
|
||||||
|
// should be pristine when started.
|
||||||
|
"must_run": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Default: true,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ForceNew is not true for image because we need to
|
||||||
|
// sane this against Docker image IDs, as each image
|
||||||
|
// can have multiple names/tags attached do it.
|
||||||
|
"image": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"hostname": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"domainname": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"command": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
},
|
||||||
|
|
||||||
|
"dns": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
Set: stringSetHash,
|
||||||
|
},
|
||||||
|
|
||||||
|
"publish_all_ports": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"volumes": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: getVolumesElem(),
|
||||||
|
Set: resourceDockerVolumesHash,
|
||||||
|
},
|
||||||
|
|
||||||
|
"ports": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: getPortsElem(),
|
||||||
|
Set: resourceDockerPortsHash,
|
||||||
|
},
|
||||||
|
|
||||||
|
"env": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
Set: stringSetHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVolumesElem() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"from_container": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"container_path": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"host_path": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"read_only": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPortsElem() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"internal": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"external": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"ip": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"protocol": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Default: "tcp",
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerPortsHash(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", m["internal"].(int)))
|
||||||
|
|
||||||
|
if v, ok := m["external"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(int)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := m["ip"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := m["protocol"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerVolumesHash(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
m := v.(map[string]interface{})
|
||||||
|
|
||||||
|
if v, ok := m["from_container"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := m["container_path"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := m["host_path"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := m["read_only"]; ok {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v-", v.(bool)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSetHash(v interface{}) int {
|
||||||
|
return hashcode.String(v.(string))
|
||||||
|
}
|
||||||
282
resource_docker_container_funcs.go
Normal file
282
resource_docker_container_funcs.go
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dc "github.com/fsouza/go-dockerclient"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
|
||||||
|
client, err := config.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to connect to Docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := config.NewData()
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
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{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
PublishAllPorts: d.Get("publish_all_ports").(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
config := meta.(*Config)
|
||||||
|
|
||||||
|
client, err := config.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to connect to Docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := client.InspectContainer(apiContainer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error inspecting container %s: %s", apiContainer.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Get("must_run").(bool) && !container.State.Running {
|
||||||
|
return resourceDockerContainerDelete(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
|
||||||
|
client, err := config.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to connect to Docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
var dockerContainerName string
|
||||||
|
if len(apiContainer.Names) > 0 {
|
||||||
|
dockerContainerName = strings.TrimLeft(apiContainer.Names[0], "/")
|
||||||
|
} else {
|
||||||
|
dockerContainerName = apiContainer.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if dockerContainerName == name {
|
||||||
|
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 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
|
||||||
|
}
|
||||||
31
resource_docker_image.go
Normal file
31
resource_docker_image.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceDockerImage() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceDockerImageCreate,
|
||||||
|
Read: resourceDockerImageRead,
|
||||||
|
Update: resourceDockerImageUpdate,
|
||||||
|
Delete: resourceDockerImageDelete,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"keep_updated": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"latest": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
177
resource_docker_image_funcs.go
Normal file
177
resource_docker_image_funcs.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dc "github.com/fsouza/go-dockerclient"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
|
||||||
|
apiImage, err := findImage(d, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(apiImage.ID + d.Get("name").(string))
|
||||||
|
d.Set("latest", apiImage.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
config := meta.(*Config)
|
||||||
|
|
||||||
|
apiImage, err := findImage(d, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("latest", apiImage.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
// We need to re-read in case switching parameters affects
|
||||||
|
// the value of "latest" or others
|
||||||
|
|
||||||
|
return resourceDockerImageRead(d, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchLocalImages(data *Data, client *dc.Client) error {
|
||||||
|
images, err := client.ListImages(dc.ListImagesOptions{All: false})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to list Docker images: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker uses different nomenclatures in different places...sometimes a short
|
||||||
|
// ID, sometimes long, etc. So we store both in the map so we can always find
|
||||||
|
// the same image object. We store the tags, too.
|
||||||
|
for i, image := range images {
|
||||||
|
data.DockerImages[image.ID[:12]] = &images[i]
|
||||||
|
data.DockerImages[image.ID] = &images[i]
|
||||||
|
for _, repotag := range image.RepoTags {
|
||||||
|
data.DockerImages[repotag] = &images[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pullImage(data *Data, client *dc.Client, image string) error {
|
||||||
|
// TODO: Test local registry handling. It should be working
|
||||||
|
// based on the code that was ported over
|
||||||
|
|
||||||
|
pullOpts := dc.PullImageOptions{}
|
||||||
|
|
||||||
|
splitImageName := strings.Split(image, ":")
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// It's in registry:port/repo:tag format
|
||||||
|
case len(splitImageName) == 3:
|
||||||
|
splitPortRepo := strings.Split(splitImageName[1], "/")
|
||||||
|
pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0]
|
||||||
|
pullOpts.Repository = splitPortRepo[1]
|
||||||
|
pullOpts.Tag = splitImageName[2]
|
||||||
|
|
||||||
|
// It's either registry:port/repo or repo:tag with default registry
|
||||||
|
case len(splitImageName) == 2:
|
||||||
|
splitPortRepo := strings.Split(splitImageName[1], "/")
|
||||||
|
switch len(splitPortRepo) {
|
||||||
|
|
||||||
|
// registry:port/repo
|
||||||
|
case 2:
|
||||||
|
pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0]
|
||||||
|
pullOpts.Repository = splitPortRepo[1]
|
||||||
|
pullOpts.Tag = "latest"
|
||||||
|
|
||||||
|
// repo:tag
|
||||||
|
case 1:
|
||||||
|
pullOpts.Repository = splitImageName[0]
|
||||||
|
pullOpts.Tag = splitImageName[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
pullOpts.Repository = image
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
|
||||||
|
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchLocalImages(data, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageTag(image string) string {
|
||||||
|
splitImageName := strings.Split(image, ":")
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// It's in registry:port/repo:tag format
|
||||||
|
case len(splitImageName) == 3:
|
||||||
|
return splitImageName[2]
|
||||||
|
|
||||||
|
// It's either registry:port/repo or repo:tag with default registry
|
||||||
|
case len(splitImageName) == 2:
|
||||||
|
splitPortRepo := strings.Split(splitImageName[1], "/")
|
||||||
|
if len(splitPortRepo) == 2 {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return splitImageName[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findImage(d *schema.ResourceData, config *Config) (*dc.APIImages, error) {
|
||||||
|
client, err := config.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to connect to Docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := config.NewData()
|
||||||
|
|
||||||
|
if err := fetchLocalImages(data, client); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageName := d.Get("name").(string)
|
||||||
|
if imageName == "" {
|
||||||
|
return nil, fmt.Errorf("Empty image name is not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLocal := func() *dc.APIImages {
|
||||||
|
if apiImage, ok := data.DockerImages[imageName]; ok {
|
||||||
|
return apiImage
|
||||||
|
}
|
||||||
|
if apiImage, ok := data.DockerImages[imageName+":latest"]; ok {
|
||||||
|
imageName = imageName + ":latest"
|
||||||
|
return apiImage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
foundImage := searchLocal()
|
||||||
|
|
||||||
|
if d.Get("keep_updated").(bool) || foundImage == nil {
|
||||||
|
if err := pullImage(data, client, imageName); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundImage = searchLocal()
|
||||||
|
if foundImage != nil {
|
||||||
|
return foundImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unable to find or pull image %s", imageName)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue