Merge branch 'master' of https://github.com/jefferai/terraform into jefferai-master
This commit is contained in:
commit
b7c88f0038
|
@ -53,8 +53,8 @@ If you have never worked with Go before, you will have to complete the
|
||||||
following steps in order to be able to compile and test Terraform (or
|
following steps in order to be able to compile and test Terraform (or
|
||||||
use the Vagrantfile in this repo to stand up a dev VM).
|
use the Vagrantfile in this repo to stand up a dev VM).
|
||||||
|
|
||||||
1. Install Go. Make sure the Go version is at least Go 1.2. Terraform will not work with anything less than
|
1. Install Go. Make sure the Go version is at least Go 1.4. Terraform will not work with anything less than
|
||||||
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2.
|
Go 1.4. On a Mac, you can `brew install go` to install Go 1.4.
|
||||||
|
|
||||||
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
||||||
For example, you can add to your `.bash_profile`.
|
For example, you can add to your `.bash_profile`.
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/builtin/providers/docker"
|
||||||
|
"github.com/hashicorp/terraform/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.Serve(&plugin.ServeOpts{
|
||||||
|
ProviderFunc: docker.Provider,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -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{},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -179,7 +179,7 @@ func (c *Config) discoverSingle(glob string, m *map[string]string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Discoverd plugin: %s = %s", parts[2], match)
|
log.Printf("[DEBUG] Discovered plugin: %s = %s", parts[2], match)
|
||||||
(*m)[parts[2]] = match
|
(*m)[parts[2]] = match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue