From 77e1e6067e43b4f435ed3d285ed277cf2dbc33ec Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 17 Feb 2015 16:28:33 +0000 Subject: [PATCH 01/74] 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. --- config.go | 24 +++ provider.go | 34 ++++ resource_docker_container.go | 222 +++++++++++++++++++++++ resource_docker_container_funcs.go | 282 +++++++++++++++++++++++++++++ resource_docker_image.go | 31 ++++ resource_docker_image_funcs.go | 177 ++++++++++++++++++ 6 files changed, 770 insertions(+) create mode 100644 config.go create mode 100644 provider.go create mode 100644 resource_docker_container.go create mode 100644 resource_docker_container_funcs.go create mode 100644 resource_docker_image.go create mode 100644 resource_docker_image_funcs.go diff --git a/config.go b/config.go new file mode 100644 index 00000000..40355b24 --- /dev/null +++ b/config.go @@ -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{}, + } +} diff --git a/provider.go b/provider.go new file mode 100644 index 00000000..d01ec385 --- /dev/null +++ b/provider.go @@ -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 +} diff --git a/resource_docker_container.go b/resource_docker_container.go new file mode 100644 index 00000000..50b501ca --- /dev/null +++ b/resource_docker_container.go @@ -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)) +} diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go new file mode 100644 index 00000000..d0daa08b --- /dev/null +++ b/resource_docker_container_funcs.go @@ -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 +} diff --git a/resource_docker_image.go b/resource_docker_image.go new file mode 100644 index 00000000..54822d73 --- /dev/null +++ b/resource_docker_image.go @@ -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, + }, + }, + } +} diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go new file mode 100644 index 00000000..a34f2199 --- /dev/null +++ b/resource_docker_image_funcs.go @@ -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) +} From cc79025a2efc3183c62a4df1ba9ba1f2cf1ae8b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Mar 2015 15:18:52 -0700 Subject: [PATCH 02/74] providers/docker: support DOCKER_CERT_PATH --- config.go | 22 ++++++++++++++++++---- provider.go | 14 +++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/config.go b/config.go index 40355b24..ed13314a 100644 --- a/config.go +++ b/config.go @@ -1,10 +1,15 @@ package docker -import dc "github.com/fsouza/go-dockerclient" +import ( + "path/filepath" + + dc "github.com/fsouza/go-dockerclient" +) type Config struct { - DockerHost string - SkipPull bool + Host string + CertPath string + SkipPull bool } type Data struct { @@ -13,7 +18,16 @@ type Data struct { // NewClient() returns a new Docker client. func (c *Config) NewClient() (*dc.Client, error) { - return dc.NewClient(c.DockerHost) + // If there is no cert information, then just return the direct client + if c.CertPath == "" { + return dc.NewClient(c.Host) + } + + // If there is cert information, load it and use it. + ca := filepath.Join(c.CertPath, "ca.pem") + cert := filepath.Join(c.CertPath, "cert.pem") + key := filepath.Join(c.CertPath, "key.pem") + return dc.NewTLSClient(c.Host, cert, key, ca) } // NewData() returns a new data struct. diff --git a/provider.go b/provider.go index d01ec385..77da4bf1 100644 --- a/provider.go +++ b/provider.go @@ -8,11 +8,18 @@ import ( func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ - "docker_host": &schema.Schema{ + "host": &schema.Schema{ Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:/run/docker.sock"), - Description: "The Docker daemon endpoint", + Description: "The Docker daemon address", + }, + + "cert_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", nil), + Description: "Path to directory with Docker TLS config", }, }, @@ -27,7 +34,8 @@ func Provider() terraform.ResourceProvider { func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ - DockerHost: d.Get("docker_host").(string), + Host: d.Get("host").(string), + CertPath: d.Get("cert_path").(string), } return &config, nil From faa9dc47f51aec92209d9a9ac7ee058a42dc2727 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Mar 2015 15:22:33 -0700 Subject: [PATCH 03/74] providers/docker: docker_image acceptance test --- provider_test.go | 36 +++++++++++++++++++++++++++++++++++ resource_docker_image_test.go | 32 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 provider_test.go create mode 100644 resource_docker_image_test.go diff --git a/provider_test.go b/provider_test.go new file mode 100644 index 00000000..d0910488 --- /dev/null +++ b/provider_test.go @@ -0,0 +1,36 @@ +package docker + +import ( + "os/exec" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "docker": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + cmd := exec.Command("docker", "version") + if err := cmd.Run(); err != nil { + t.Fatalf("Docker must be available: %s", err) + } +} diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go new file mode 100644 index 00000000..d43c81ef --- /dev/null +++ b/resource_docker_image_test.go @@ -0,0 +1,32 @@ +package docker + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDockerImage_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "docker_image.foo", + "latest", + "d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07"), + ), + }, + }, + }) +} + +const testAccDockerImageConfig = ` +resource "docker_image" "foo" { + name = "ubuntu:trusty-20150320" + keep_updated = true +} +` From 935647c4bc675ee3838aaf4d18b0e63debd2b8da Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Mar 2015 15:33:17 -0700 Subject: [PATCH 04/74] providers/docker: container acceptance tests --- resource_docker_container_test.go | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 resource_docker_container_test.go diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go new file mode 100644 index 00000000..9e2ef641 --- /dev/null +++ b/resource_docker_container_test.go @@ -0,0 +1,36 @@ +package docker + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDockerContainer_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "docker_image.foo", + "latest", + "d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07"), + ), + }, + }, + }) +} + +const testAccDockerContainerConfig = ` +resource "docker_image" "foo" { + name = "ubuntu:trusty-20150320" +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" +} +` From 75a5bedad0c66334f3187ca3d2f29e6df66a6568 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Mar 2015 18:37:20 -0700 Subject: [PATCH 05/74] providres/docker: cache client --- config.go | 15 +++++---------- provider.go | 2 +- resource_docker_container_funcs.go | 27 ++++++--------------------- resource_docker_image_funcs.go | 28 ++++++++++++---------------- 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/config.go b/config.go index ed13314a..19918274 100644 --- a/config.go +++ b/config.go @@ -6,14 +6,11 @@ import ( dc "github.com/fsouza/go-dockerclient" ) +// Config is the structure that stores the configuration to talk to a +// Docker API compatible host. type Config struct { Host string CertPath string - SkipPull bool -} - -type Data struct { - DockerImages map[string]*dc.APIImages } // NewClient() returns a new Docker client. @@ -30,9 +27,7 @@ func (c *Config) NewClient() (*dc.Client, error) { return dc.NewTLSClient(c.Host, cert, key, ca) } -// NewData() returns a new data struct. -func (c *Config) NewData() *Data { - return &Data{ - DockerImages: map[string]*dc.APIImages{}, - } +// Data ia structure for holding data that we fetch from Docker. +type Data struct { + DockerImages map[string]*dc.APIImages } diff --git a/provider.go b/provider.go index 77da4bf1..15bc840a 100644 --- a/provider.go +++ b/provider.go @@ -38,5 +38,5 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { CertPath: d.Get("cert_path").(string), } - return &config, nil + return config.NewClient() } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index d0daa08b..17a8e4ee 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -11,16 +11,11 @@ import ( ) func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) + var err error + client := meta.(*dc.Client) - 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 { + var data Data + if err := fetchLocalImages(&data, client); err != nil { return err } @@ -116,12 +111,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } 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) - } + client := meta.(*dc.Client) apiContainer, err := fetchDockerContainer(d.Get("name").(string), client) if err != nil { @@ -152,12 +142,7 @@ func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) err } 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) - } + client := meta.(*dc.Client) removeOpts := dc.RemoveContainerOptions{ ID: d.Id(), diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index a34f2199..2c7470db 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -9,9 +9,8 @@ import ( ) func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - apiImage, err := findImage(d, config) + client := meta.(*dc.Client) + apiImage, err := findImage(d, client) if err != nil { return fmt.Errorf("Unable to read Docker image into resource: %s", err) } @@ -23,9 +22,8 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error { } func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - apiImage, err := findImage(d, config) + client := meta.(*dc.Client) + apiImage, err := findImage(d, client) if err != nil { return fmt.Errorf("Unable to read Docker image into resource: %s", err) } @@ -53,6 +51,10 @@ func fetchLocalImages(data *Data, client *dc.Client) error { return fmt.Errorf("Unable to list Docker images: %s", err) } + if data.DockerImages == nil { + data.DockerImages = make(map[string]*dc.APIImages) + } + // 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. @@ -132,15 +134,9 @@ func getImageTag(image string) string { 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 { +func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) { + var data Data + if err := fetchLocalImages(&data, client); err != nil { return nil, err } @@ -163,7 +159,7 @@ func findImage(d *schema.ResourceData, config *Config) (*dc.APIImages, error) { foundImage := searchLocal() if d.Get("keep_updated").(bool) || foundImage == nil { - if err := pullImage(data, client, imageName); err != nil { + if err := pullImage(&data, client, imageName); err != nil { return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err) } } From 60c19680d7ce8925df81863e9b1fd081a7c1e512 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Mar 2015 18:45:36 -0700 Subject: [PATCH 06/74] providers/docker: make container test better --- resource_docker_container_test.go | 35 +++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 9e2ef641..48302d09 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -1,9 +1,12 @@ package docker import ( + "fmt" "testing" + dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccDockerContainer_basic(t *testing.T) { @@ -14,16 +17,40 @@ func TestAccDockerContainer_basic(t *testing.T) { resource.TestStep{ Config: testAccDockerContainerConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "docker_image.foo", - "latest", - "d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07"), + testAccContainerRunning("docker_container.foo"), ), }, }, }) } +func testAccContainerRunning(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*dc.Client) + containers, err := client.ListContainers(dc.ListContainersOptions{}) + if err != nil { + return err + } + + for _, c := range containers { + if c.ID == rs.Primary.ID { + return nil + } + } + + return fmt.Errorf("Container not found: %s", rs.Primary.ID) + } +} + const testAccDockerContainerConfig = ` resource "docker_image" "foo" { name = "ubuntu:trusty-20150320" From 976410e44dfc2dab60aa95bab315bce548d0b08c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Mar 2015 19:06:48 -0700 Subject: [PATCH 07/74] providers/docker: ping docker server on startup --- provider.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/provider.go b/provider.go index 15bc840a..2fe456e9 100644 --- a/provider.go +++ b/provider.go @@ -1,6 +1,8 @@ package docker import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) @@ -38,5 +40,15 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { CertPath: d.Get("cert_path").(string), } - return config.NewClient() + client, err := config.NewClient() + if err != nil { + return nil, fmt.Errorf("Error initializing Docker client: %s", err) + } + + err = client.Ping() + if err != nil { + return nil, fmt.Errorf("Error pinging Docker server: %s", err) + } + + return client, nil } From 518f7b6ffa094b0fc5a80887be58b95c1b1161eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Apr 2015 09:49:03 -0700 Subject: [PATCH 08/74] providers/docker: default cert_path to non-nil so input isn't asked --- provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider.go b/provider.go index 2fe456e9..fdc8b771 100644 --- a/provider.go +++ b/provider.go @@ -20,7 +20,7 @@ func Provider() terraform.ResourceProvider { "cert_path": &schema.Schema{ Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", nil), + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", ""), Description: "Path to directory with Docker TLS config", }, }, From 2c7fb5fcc796977f9748192cae4b56ee28172b3f Mon Sep 17 00:00:00 2001 From: Nick Downs Date: Thu, 16 Apr 2015 12:42:21 -0700 Subject: [PATCH 09/74] Added Docker links support to the docker_container resource. --- resource_docker_container.go | 8 ++++++++ resource_docker_container_funcs.go | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 50b501ca..de0b0b66 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -108,6 +108,14 @@ func resourceDockerContainer() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Set: stringSetHash, }, + + "links": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: stringSetHash, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 17a8e4ee..ae0a6e6b 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -103,6 +103,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.DNS = stringSetToStringSlice(v.(*schema.Set)) } + if v, ok := d.GetOk("links"); ok { + hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) + } + if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) } From 8cbb3adce4860241f18119cdaef9a5e945756b86 Mon Sep 17 00:00:00 2001 From: Stephan Epping Date: Thu, 16 Apr 2015 15:21:14 +0200 Subject: [PATCH 10/74] Add docker container network settings to output attribute --- resource_docker_container.go | 20 ++++++++++++++++++++ resource_docker_container_funcs.go | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index de0b0b66..a3a9b492 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -116,6 +116,26 @@ func resourceDockerContainer() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Set: stringSetHash, }, + + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "ip_prefix_length": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + + "gateway": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "bridge": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index ae0a6e6b..d4fd4c9d 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -138,6 +138,12 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error return resourceDockerContainerDelete(d, meta) } + // Read Network Settings + 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 } From 9da91b13045184e4b6e5c46ae1bf31a994d23f10 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Mon, 20 Apr 2015 12:42:36 -0500 Subject: [PATCH 11/74] provider/docker: guard against nil NetworkSettings --- resource_docker_container_funcs.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index d4fd4c9d..c6bd9dea 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -138,11 +138,13 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error return resourceDockerContainerDelete(d, meta) } - // Read Network Settings - 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) + // 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 } From d9b3a99ef603efc3c5028feb99d42cef01eb8ae7 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Mon, 20 Apr 2015 14:18:46 -0500 Subject: [PATCH 12/74] provider/docker: fmt on container resource --- resource_docker_container.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index a3a9b492..10481b26 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -117,25 +117,25 @@ func resourceDockerContainer() *schema.Resource { Set: stringSetHash, }, - "ip_address": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, - "ip_prefix_length": &schema.Schema{ - Type: schema.TypeInt, - Computed: true, - }, + "ip_prefix_length": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, - "gateway": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, + "gateway": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, - "bridge": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, + "bridge": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } From 2659e028c0ce7998251aa606996ff98b1f4d5da1 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Tue, 5 May 2015 23:22:09 +0000 Subject: [PATCH 13/74] Added support for more complexly images repos such as images on a private registry that are stored as namespace/name --- resource_docker_image_funcs.go | 46 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index 2c7470db..8fb58ba2 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -3,6 +3,7 @@ package docker import ( "fmt" "strings" + "regexp" dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/schema" @@ -75,35 +76,28 @@ func pullImage(data *Data, client *dc.Client, image string) error { pullOpts := dc.PullImageOptions{} - splitImageName := strings.Split(image, ":") - switch { + // + // Breaks apart an image string into host:port, repo, and tag components + regex := "^(?:(?P(?:[\\w-]+(?:\\.[\\w-]+)+)(?::[\\d]+)?)/)?(?P[\\w.-]+(?:/[\\w.-]*)*)*(?::(?P[\\w.-]*))?" + r, _ := regexp.Compile(regex) - // 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] + // Result is in form [[image, host, repo, tag]], so we get the head of the + // outer list to pass the inner list to result + result := r.FindAllStringSubmatch(image, -1)[0] - // It's either registry:port/repo or repo:tag with default registry - case len(splitImageName) == 2: - splitPortRepo := strings.Split(splitImageName[1], "/") - switch len(splitPortRepo) { + // If the host is not an empty string, then the image is using a private registry + if (result[1] != "") { + pullOpts.Registry = result[1] + // The repository for a private registry should take the form of host/repo rather than just repo + pullOpts.Repository = result[1] + "/" + result[2] + } else if (result[2] != "") { + // Local registries, or the main docker registry will have an image named as just 'repo' + pullOpts.Repository = result[2] + } - // 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 there was a tag specified, then set it + if (result[3] != "") { + pullOpts.Tag = result[3] } if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil { From 1c80ff0ac9398201cb43d2f09fb96a0eceaac0aa Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 7 May 2015 09:50:16 -0500 Subject: [PATCH 14/74] provider/docker: update image sha Should eventually see if there's a way to rework this so it's less brittle. But for now, we band-aid! --- resource_docker_image_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index d43c81ef..14dfb29b 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -17,7 +17,7 @@ func TestAccDockerImage_basic(t *testing.T) { resource.TestCheckResourceAttr( "docker_image.foo", "latest", - "d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07"), + "b7cf8f0d9e82c9d96bd7afd22c600bfdb86b8d66c50d29164e5ad2fb02f7187b"), ), }, }, From b440ebc4924c9e07ace6d67ae921e789a8a140d5 Mon Sep 17 00:00:00 2001 From: Julian Schneider Date: Thu, 4 Jun 2015 12:57:38 +0200 Subject: [PATCH 15/74] Add privileged option to docker container resource --- resource_docker_container.go | 6 ++++++ resource_docker_container_funcs.go | 1 + 2 files changed, 7 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 10481b26..59e65b9c 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -136,6 +136,12 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "privileged": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index c6bd9dea..4f642d6d 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -85,6 +85,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) hostConfig := &dc.HostConfig{ + Privileged: d.Get("privileged").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool), } From 383d50b35d6b2beeb53ac7559a1c9676c9e2d018 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 12 Jun 2015 19:25:16 +0000 Subject: [PATCH 16/74] This puts the image parsing code (mostly) back to how it was before. The regex solution is extremely complex, which makes it hard to debug and understand; the original switches and commenting lay out the various cases in a straightforward fashion. Plus, implementing namespace/repo support in the original code was a simple strings.Join call. --- resource_docker_image_funcs.go | 47 +++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index 8fb58ba2..f45dd222 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -3,7 +3,6 @@ package docker import ( "fmt" "strings" - "regexp" dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/schema" @@ -76,28 +75,36 @@ func pullImage(data *Data, client *dc.Client, image string) error { pullOpts := dc.PullImageOptions{} - // - // Breaks apart an image string into host:port, repo, and tag components - regex := "^(?:(?P(?:[\\w-]+(?:\\.[\\w-]+)+)(?::[\\d]+)?)/)?(?P[\\w.-]+(?:/[\\w.-]*)*)*(?::(?P[\\w.-]*))?" - r, _ := regexp.Compile(regex) + splitImageName := strings.Split(image, ":") + switch len(splitImageName) { - // Result is in form [[image, host, repo, tag]], so we get the head of the - // outer list to pass the inner list to result - result := r.FindAllStringSubmatch(image, -1)[0] + // It's in registry:port/username/repo:tag or registry:port/repo:tag format + case 3: + splitPortRepo := strings.Split(splitImageName[1], "/") + pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0] + pullOpts.Tag = splitImageName[2] + pullOpts.Repository = strings.Join(splitPortRepo[1:], "/") - // If the host is not an empty string, then the image is using a private registry - if (result[1] != "") { - pullOpts.Registry = result[1] - // The repository for a private registry should take the form of host/repo rather than just repo - pullOpts.Repository = result[1] + "/" + result[2] - } else if (result[2] != "") { - // Local registries, or the main docker registry will have an image named as just 'repo' - pullOpts.Repository = result[2] - } + // It's either registry:port/username/repo, registry:port/repo, + // or repo:tag with default registry + case 2: + splitPortRepo := strings.Split(splitImageName[1], "/") + switch len(splitPortRepo) { + // repo:tag + case 1: + pullOpts.Repository = splitImageName[0] + pullOpts.Tag = splitImageName[1] - // If there was a tag specified, then set it - if (result[3] != "") { - pullOpts.Tag = result[3] + // registry:port/username/repo or registry:port/repo + default: + pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0] + pullOpts.Repository = strings.Join(splitPortRepo[1:], "/") + pullOpts.Tag = "latest" + } + + // Plain username/repo or repo + default: + pullOpts.Repository = image } if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil { From c2dc6c823461bc08135261a725300dfbb587db0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Jun 2015 22:31:24 -0700 Subject: [PATCH 17/74] fmt --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 4f642d6d..9408d8a6 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -85,7 +85,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) hostConfig := &dc.HostConfig{ - Privileged: d.Get("privileged").(bool), + Privileged: d.Get("privileged").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool), } From 40fed60ec4d30067f33aad439218cf952933bfe7 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 12 Jun 2015 18:44:37 +0000 Subject: [PATCH 18/74] When linking to other containers, introduce a slight delay; this lets the Docker API get those containers running. Otherwise when you try to start a container linking to them, the start command will fail, leading to an error. --- resource_docker_container_funcs.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 9408d8a6..d355b898 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "time" dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/schema" @@ -19,6 +20,8 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err return err } + delayStart := false + image := d.Get("image").(string) if _, ok := data.DockerImages[image]; !ok { if _, ok := data.DockerImages[image+":latest"]; !ok { @@ -106,6 +109,13 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err if v, ok := d.GetOk("links"); ok { hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) + delayStart = true + } + + // For instance, Docker will fail to start conatiners with links + // to other containers if the containers haven't started yet + if delayStart { + time.Sleep(3 * time.Second) } if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { From fc6ef37d95bb1678509e5f8a8af2d4cbe170e523 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 12 Jun 2015 21:01:36 +0000 Subject: [PATCH 19/74] Fix a serious problem when using links. Links cause there to be more than one name for a container to be returned. As a result, only looking at the first element of the container names could cause a container to not be found, leading Terraform to remove it from state and attempt to recreate it. --- resource_docker_container_funcs.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index d355b898..779ce1d1 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -211,15 +211,17 @@ func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, er // 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 + 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 + } + } } } From 8b4e37a42c32dee19275c9f6fa2ba7499b4bc9ae Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 25 Jun 2015 14:38:56 +0000 Subject: [PATCH 20/74] As discussed on the issue, remove the hard-coded delay on startup in favor of attempting to detect if the initial container ever enters running state, and erroring out if not. It will re-check the container once every 500ms for 15 seconds total; future work could make that configurable. --- resource_docker_container_funcs.go | 59 +++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 779ce1d1..058a4411 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -11,6 +11,10 @@ import ( "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) @@ -20,15 +24,12 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err return err } - delayStart := false - 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" } + image = image + ":latest" } // The awesome, wonderful, splendiferous, sensical @@ -109,15 +110,9 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err if v, ok := d.GetOk("links"); ok { hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) - delayStart = true - } - - // For instance, Docker will fail to start conatiners with links - // to other containers if the containers haven't started yet - if delayStart { - time.Sleep(3 * time.Second) } + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) } @@ -132,21 +127,49 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error 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) + 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) } - if d.Get("must_run").(bool) && !container.State.Running { - return resourceDockerContainerDelete(d, meta) + // 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 From d66d85b5e2be537948178e759e90029f4559b2d6 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Mon, 29 Jun 2015 16:09:05 -0500 Subject: [PATCH 21/74] provider/docker: [tests] change images use a base image with a long running process - fixes container tests --- resource_docker_container_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 48302d09..29ecc4bb 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -53,7 +53,7 @@ func testAccContainerRunning(n string) resource.TestCheckFunc { const testAccDockerContainerConfig = ` resource "docker_image" "foo" { - name = "ubuntu:trusty-20150320" + name = "nginx:latest" } resource "docker_container" "foo" { From 4bb21500a9cfcadd513e5677d75117a5c3262d27 Mon Sep 17 00:00:00 2001 From: Matti Savolainen Date: Fri, 3 Jul 2015 12:58:05 +0300 Subject: [PATCH 22/74] Fix Repository attribute in docker client PullOptions for private registries. --- resource_docker_image_funcs.go | 4 ++-- resource_docker_image_test.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index f45dd222..454113c5 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -83,7 +83,7 @@ func pullImage(data *Data, client *dc.Client, image string) error { splitPortRepo := strings.Split(splitImageName[1], "/") pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0] pullOpts.Tag = splitImageName[2] - pullOpts.Repository = strings.Join(splitPortRepo[1:], "/") + pullOpts.Repository = pullOpts.Registry + "/" + strings.Join(splitPortRepo[1:], "/") // It's either registry:port/username/repo, registry:port/repo, // or repo:tag with default registry @@ -98,7 +98,7 @@ func pullImage(data *Data, client *dc.Client, image string) error { // registry:port/username/repo or registry:port/repo default: pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0] - pullOpts.Repository = strings.Join(splitPortRepo[1:], "/") + pullOpts.Repository = pullOpts.Registry + "/" + strings.Join(splitPortRepo[1:], "/") pullOpts.Tag = "latest" } diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 14dfb29b..844b5632 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -1,9 +1,8 @@ package docker import ( - "testing" - "github.com/hashicorp/terraform/helper/resource" + "testing" ) func TestAccDockerImage_basic(t *testing.T) { @@ -24,9 +23,34 @@ func TestAccDockerImage_basic(t *testing.T) { }) } +func TestAddDockerImage_private(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAddDockerPrivateImageConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "docker_image.foobar", + "latest", + "2c40b0526b6358710fd09e7b8c022429268cc61703b4777e528ac9d469a07ca1"), + ), + }, + }, + }) +} + const testAccDockerImageConfig = ` resource "docker_image" "foo" { name = "ubuntu:trusty-20150320" keep_updated = true } ` + +const testAddDockerPrivateImageConfig = ` +resource "docker_image" "foobar" { + name = "gcr.io:443/google_containers/pause:0.8.0" + keep_updated = true +} +` From f229be7a608a53fc451093b0f1cac0033087c5ef Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 8 Oct 2015 09:47:50 +0100 Subject: [PATCH 23/74] Gofmt change for resource docker_image test --- resource_docker_image_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 844b5632..0f0f0707 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -1,8 +1,9 @@ package docker import ( - "github.com/hashicorp/terraform/helper/resource" "testing" + + "github.com/hashicorp/terraform/helper/resource" ) func TestAccDockerImage_basic(t *testing.T) { From ce511d53dbfbd0a5f74d4e2df297d338abebb829 Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Thu, 8 Oct 2015 15:48:04 +0300 Subject: [PATCH 24/74] removed extra parentheses --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 058a4411..aa74a4e1 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -148,7 +148,7 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error } if container.State.Running || - (!container.State.Running && !d.Get("must_run").(bool)) { + !container.State.Running && !d.Get("must_run").(bool) { break } From 4b257e6b2b0dd4a071936f8c1288458e6656499d Mon Sep 17 00:00:00 2001 From: ryane Date: Mon, 26 Oct 2015 17:24:48 -0400 Subject: [PATCH 25/74] entrypoint support for docker_container resource --- resource_docker_container.go | 7 +++++ resource_docker_container_funcs.go | 4 +++ resource_docker_container_test.go | 49 ++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 59e65b9c..4fe63650 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -71,6 +71,13 @@ func resourceDockerContainer() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, + "entrypoint": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dns": &schema.Schema{ Type: schema.TypeSet, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index aa74a4e1..24df6949 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -54,6 +54,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{})) } + if v, ok := d.GetOk("entrypoint"); ok { + createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{})) + } + exposedPorts := map[dc.Port]struct{}{} portBindings := map[dc.Port][]dc.PortBinding{} diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 29ecc4bb..e888c67d 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -10,6 +10,7 @@ import ( ) func TestAccDockerContainer_basic(t *testing.T) { + var c dc.Container resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -17,14 +18,42 @@ func TestAccDockerContainer_basic(t *testing.T) { resource.TestStep{ Config: testAccDockerContainerConfig, Check: resource.ComposeTestCheckFunc( - testAccContainerRunning("docker_container.foo"), + testAccContainerRunning("docker_container.foo", &c), ), }, }, }) } -func testAccContainerRunning(n string) resource.TestCheckFunc { +func TestAccDockerContainer_entrypoint(t *testing.T) { + var c dc.Container + + testCheck := func(*terraform.State) error { + if len(c.Config.Entrypoint) < 3 || + (c.Config.Entrypoint[0] != "/bin/bash" && + c.Config.Entrypoint[1] != "-c" && + c.Config.Entrypoint[2] != "ping localhost") { + return fmt.Errorf("Container wrong entrypoint: %s", c.Config.Entrypoint) + } + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerEntrypointConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + ), + }, + }, + }) +} + +func testAccContainerRunning(n string, container *dc.Container) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -43,6 +72,11 @@ func testAccContainerRunning(n string) resource.TestCheckFunc { for _, c := range containers { if c.ID == rs.Primary.ID { + inspected, err := client.InspectContainer(c.ID) + if err != nil { + return fmt.Errorf("Container could not be inspected: %s", err) + } + *container = *inspected return nil } } @@ -61,3 +95,14 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" } ` +const testAccDockerContainerEntrypointConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + entrypoint = ["/bin/bash", "-c", "ping localhost"] +} +` From 311b164d562bf04dc73db4777840ce36c1e443d9 Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 27 Oct 2015 12:08:57 -0400 Subject: [PATCH 26/74] restart policy support for docker_container --- resource_docker_container.go | 22 ++++++++++++++++++++++ resource_docker_container_funcs.go | 4 ++++ resource_docker_container_test.go | 17 ++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 4fe63650..7d2fa34c 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" + "regexp" ) func resourceDockerContainer() *schema.Resource { @@ -92,6 +93,27 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "restart": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "no", + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + if !regexp.MustCompile(`^(no|on-failure|always)$`).MatchString(value) { + es = append(es, fmt.Errorf( + "%q must be one of \"no\", \"on-failure\", or \"always\"", k)) + } + return + }, + }, + + "max_retry_count": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "volumes": &schema.Schema{ Type: schema.TypeSet, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 24df6949..800f0f8a 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -95,6 +95,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err 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), + }, } if len(portBindings) != 0 { diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index e888c67d..0d0fe734 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -25,7 +25,7 @@ func TestAccDockerContainer_basic(t *testing.T) { }) } -func TestAccDockerContainer_entrypoint(t *testing.T) { +func TestAccDockerContainer_customized(t *testing.T) { var c dc.Container testCheck := func(*terraform.State) error { @@ -35,6 +35,15 @@ func TestAccDockerContainer_entrypoint(t *testing.T) { c.Config.Entrypoint[2] != "ping localhost") { return fmt.Errorf("Container wrong entrypoint: %s", c.Config.Entrypoint) } + + if c.HostConfig.RestartPolicy.Name == "on-failure" { + if c.HostConfig.RestartPolicy.MaximumRetryCount != 5 { + return fmt.Errorf("Container has wrong restart policy max retry count: %d", c.HostConfig.RestartPolicy.MaximumRetryCount) + } + } else { + return fmt.Errorf("Container has wrong restart policy: %s", c.HostConfig.RestartPolicy.Name) + } + return nil } @@ -43,7 +52,7 @@ func TestAccDockerContainer_entrypoint(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccDockerContainerEntrypointConfig, + Config: testAccDockerContainerCustomizedConfig, Check: resource.ComposeTestCheckFunc( testAccContainerRunning("docker_container.foo", &c), testCheck, @@ -95,7 +104,7 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" } ` -const testAccDockerContainerEntrypointConfig = ` +const testAccDockerContainerCustomizedConfig = ` resource "docker_image" "foo" { name = "nginx:latest" } @@ -104,5 +113,7 @@ resource "docker_container" "foo" { name = "tf-test" image = "${docker_image.foo.latest}" entrypoint = ["/bin/bash", "-c", "ping localhost"] + restart = "on-failure" + max_retry_count = 5 } ` From 38eec5fbc7babc5905ae60e33988b0c96e14fdd2 Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 27 Oct 2015 19:53:49 -0400 Subject: [PATCH 27/74] add basic runtime constraints to docker_container --- resource_docker_container.go | 18 ++++++++++++++++++ resource_docker_container_funcs.go | 24 ++++++++++++++++++++++++ resource_docker_container_test.go | 14 ++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 7d2fa34c..48eac9a4 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -171,6 +171,24 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, }, + + "memory": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "memory_swap": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "cpu_shares": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 800f0f8a..0f1a9d9e 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -120,6 +120,30 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) } + 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 + } + } + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 0d0fe734..1402f129 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -44,6 +44,17 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container has wrong restart policy: %s", c.HostConfig.RestartPolicy.Name) } + if c.HostConfig.Memory != (128 * 1024 * 1024) { + return fmt.Errorf("Container has wrong memory setting: %d", c.HostConfig.Memory) + } + + if c.HostConfig.MemorySwap != (128 * 1024 * 1024) { + return fmt.Errorf("Container has wrong memory swap setting: %d", c.HostConfig.Memory) + } + + if c.HostConfig.CPUShares != 512 { + return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) + } return nil } @@ -115,5 +126,8 @@ resource "docker_container" "foo" { entrypoint = ["/bin/bash", "-c", "ping localhost"] restart = "on-failure" max_retry_count = 5 + memory = 128 + memory_swap = 128 + cpu_shares = 512 } ` From 8f9b15181177ac575be09e453d7d0e31cdff956b Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 3 Nov 2015 15:20:58 -0500 Subject: [PATCH 28/74] add label support to docker container resource --- resource_docker_container.go | 6 ++++++ resource_docker_container_funcs.go | 12 ++++++++++++ resource_docker_container_test.go | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 48eac9a4..0a29ab73 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -172,6 +172,12 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "labels": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + "memory": &schema.Schema{ Type: schema.TypeInt, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 0f1a9d9e..4a617480 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -82,6 +82,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err createOpts.Config.Volumes = volumes } + if v, ok := d.GetOk("labels"); ok { + createOpts.Config.Labels = mapLabels(v.(map[string]interface{})) + } + var retContainer *dc.Container if retContainer, err = client.CreateContainer(createOpts); err != nil { return fmt.Errorf("Unable to create container: %s", err) @@ -255,6 +259,14 @@ func stringSetToStringSlice(stringSet *schema.Set) []string { return ret } +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 +} + func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) { apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true}) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 1402f129..e194d1a1 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -55,6 +55,11 @@ func TestAccDockerContainer_customized(t *testing.T) { if c.HostConfig.CPUShares != 512 { return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) } + + if c.Config.Labels["env"] != "prod" || c.Config.Labels["role"] != "test" { + return fmt.Errorf("Container does not have the correct labels") + } + return nil } @@ -129,5 +134,9 @@ resource "docker_container" "foo" { memory = 128 memory_swap = 128 cpu_shares = 512 + labels { + env = "prod" + role = "test" + } } ` From 38b1e1ce2615b5ad71f257646166b37d1b67f9b8 Mon Sep 17 00:00:00 2001 From: ryane Date: Wed, 4 Nov 2015 12:42:55 -0500 Subject: [PATCH 29/74] support for log driver + config in docker container --- resource_docker_container.go | 21 +++++++++++++++++++++ resource_docker_container_funcs.go | 15 +++++++++++---- resource_docker_container_test.go | 17 +++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 0a29ab73..92331fc7 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -195,6 +195,27 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, }, + + "log_driver": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "json-file", + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + if !regexp.MustCompile(`^(json-file|syslog|journald|gelf|fluentd)$`).MatchString(value) { + es = append(es, fmt.Errorf( + "%q must be one of \"json-file\", \"syslog\", \"journald\", \"gelf\", or \"fluentd\"", k)) + } + return + }, + }, + + "log_opts": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 4a617480..443f9ef3 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -83,7 +83,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } if v, ok := d.GetOk("labels"); ok { - createOpts.Config.Labels = mapLabels(v.(map[string]interface{})) + createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{})) } var retContainer *dc.Container @@ -103,6 +103,9 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err 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 { @@ -148,6 +151,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } } + if v, ok := d.GetOk("log_opts"); ok { + hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{})) + } + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) @@ -259,9 +266,9 @@ func stringSetToStringSlice(stringSet *schema.Set) []string { return ret } -func mapLabels(labels map[string]interface{}) map[string]string { - mapped := make(map[string]string, len(labels)) - for k, v := range labels { +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 diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index e194d1a1..4b3dfce9 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -60,6 +60,18 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container does not have the correct labels") } + if c.HostConfig.LogConfig.Type != "json-file" { + return fmt.Errorf("Container does not have the correct log config: %s", c.HostConfig.LogConfig.Type) + } + + if c.HostConfig.LogConfig.Config["max-size"] != "10m" { + return fmt.Errorf("Container does not have the correct max-size log option: %v", c.HostConfig.LogConfig.Config["max-size"]) + } + + if c.HostConfig.LogConfig.Config["max-file"] != "20" { + return fmt.Errorf("Container does not have the correct max-file log option: %v", c.HostConfig.LogConfig.Config["max-file"]) + } + return nil } @@ -138,5 +150,10 @@ resource "docker_container" "foo" { env = "prod" role = "test" } + log_driver = "json-file" + log_opts = { + max-size = "10m" + max-file = 20 + } } ` From f6e4608a35f2aed4bd47fbd50e9205a5c6efa6ab Mon Sep 17 00:00:00 2001 From: ryane Date: Wed, 4 Nov 2015 15:46:24 -0500 Subject: [PATCH 30/74] fix resource constraint specs --- resource_docker_container_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 4b3dfce9..df8ba0cb 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -44,15 +44,15 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container has wrong restart policy: %s", c.HostConfig.RestartPolicy.Name) } - if c.HostConfig.Memory != (128 * 1024 * 1024) { + if c.HostConfig.Memory != (512 * 1024 * 1024) { return fmt.Errorf("Container has wrong memory setting: %d", c.HostConfig.Memory) } - if c.HostConfig.MemorySwap != (128 * 1024 * 1024) { - return fmt.Errorf("Container has wrong memory swap setting: %d", c.HostConfig.Memory) + if c.HostConfig.MemorySwap != (2048 * 1024 * 1024) { + return fmt.Errorf("Container has wrong memory swap setting: %d", c.HostConfig.MemorySwap) } - if c.HostConfig.CPUShares != 512 { + if c.HostConfig.CPUShares != 32 { return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) } @@ -143,9 +143,9 @@ resource "docker_container" "foo" { entrypoint = ["/bin/bash", "-c", "ping localhost"] restart = "on-failure" max_retry_count = 5 - memory = 128 - memory_swap = 128 - cpu_shares = 512 + memory = 512 + memory_swap = 2048 + cpu_shares = 32 labels { env = "prod" role = "test" From f1da40e648d2d696ce80178cbc306b0acfc5d976 Mon Sep 17 00:00:00 2001 From: ryane Date: Wed, 4 Nov 2015 15:46:41 -0500 Subject: [PATCH 31/74] include hostconfig when creating docker_container --- resource_docker_container_funcs.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 443f9ef3..2b0259bc 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -86,16 +86,6 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{})) } - 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{ Privileged: d.Get("privileged").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool), @@ -155,6 +145,18 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{})) } + 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) + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) From 5a5f5ff98823dc0b0a3aefd2d96a93a43a59fe97 Mon Sep 17 00:00:00 2001 From: ryane Date: Mon, 9 Nov 2015 19:36:23 -0500 Subject: [PATCH 32/74] docker: improve validation of runtime constraints --- resource_docker_container.go | 21 +++++++++++++++++++++ resource_docker_container_funcs.go | 18 +++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 92331fc7..242462e1 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -182,18 +182,39 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeInt, Optional: true, ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(int) + if value < 0 { + es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k)) + } + return + }, }, "memory_swap": &schema.Schema{ Type: schema.TypeInt, Optional: true, ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(int) + if value < -1 { + es = append(es, fmt.Errorf("%q must be greater than or equal to -1", k)) + } + return + }, }, "cpu_shares": &schema.Schema{ Type: schema.TypeInt, Optional: true, ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(int) + if value < 0 { + es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k)) + } + return + }, }, "log_driver": &schema.Schema{ diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 2b0259bc..b0c262df 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -118,27 +118,19 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } if v, ok := d.GetOk("memory"); ok { - memory := int64(v.(int)) - if memory > 0 { - hostConfig.Memory = memory * 1024 * 1024 - } + hostConfig.Memory = int64(v.(int)) * 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 swap > 0 { + 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 - } + hostConfig.CPUShares = int64(v.(int)) } if v, ok := d.GetOk("log_opts"); ok { From c0c51b84efead8d026001d95331edcc36ab2bedf Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Fri, 20 Nov 2015 09:58:03 -0600 Subject: [PATCH 33/74] provider/docker: fix image test there's a new latest in town --- resource_docker_image_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 0f0f0707..b902749d 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -17,7 +17,7 @@ func TestAccDockerImage_basic(t *testing.T) { resource.TestCheckResourceAttr( "docker_image.foo", "latest", - "b7cf8f0d9e82c9d96bd7afd22c600bfdb86b8d66c50d29164e5ad2fb02f7187b"), + "d52aff8195301dba95e8e3d14f0c3738a874237afd54233d250a2fc4489bfa83"), ), }, }, From 5e1c8c88f57aa16e441bfdc0ce77d621d73a3b8f Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 2 Dec 2015 15:03:29 -0500 Subject: [PATCH 34/74] provider/docker: Refer to a tag instead of latest This should make tests more stable going forward. Also switch out the image used from Ubuntu to Alpine Linux to reduce required download size during test runs. --- resource_docker_image_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index b902749d..3a1c5b13 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -17,7 +17,7 @@ func TestAccDockerImage_basic(t *testing.T) { resource.TestCheckResourceAttr( "docker_image.foo", "latest", - "d52aff8195301dba95e8e3d14f0c3738a874237afd54233d250a2fc4489bfa83"), + "8dd8107abd2e22bfd3b45b05733f3d2677d4078b09b5edce56ee3d8677d3c648"), ), }, }, @@ -44,8 +44,8 @@ func TestAddDockerImage_private(t *testing.T) { const testAccDockerImageConfig = ` resource "docker_image" "foo" { - name = "ubuntu:trusty-20150320" - keep_updated = true + name = "alpine:3.1" + keep_updated = false } ` From aa872d07c6dff2d8797b3ee77c89e52870bc8c40 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 2 Dec 2015 17:27:24 -0500 Subject: [PATCH 35/74] provider/docker: locate container via ID not name This reapplies the patch mentioned in #3364 - for an unknown reason the diff there was incorrect. --- resource_docker_container_funcs.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index b0c262df..814941bb 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "strconv" - "strings" "time" dc "github.com/fsouza/go-dockerclient" @@ -160,7 +159,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*dc.Client) - apiContainer, err := fetchDockerContainer(d.Get("name").(string), client) + apiContainer, err := fetchDockerContainer(d.Id(), client) if err != nil { return err } @@ -268,7 +267,7 @@ func mapTypeMapValsToString(typeMap map[string]interface{}) map[string]string { return mapped } -func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) { +func fetchDockerContainer(ID string, client *dc.Client) (*dc.APIContainers, error) { apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true}) if err != nil { @@ -276,20 +275,8 @@ func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, er } 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. - 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 - } - } + if apiContainer.ID == ID { + return &apiContainer, nil } } From 7bfa5f90b9c1f026d93d6c0675ce340249a4f669 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Fri, 9 Oct 2015 14:05:43 +0100 Subject: [PATCH 36/74] provider/docker: Add hosts parameter for containers --- resource_docker_container.go | 37 ++++++++++++++++++++++++++++++ resource_docker_container_funcs.go | 22 +++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 242462e1..6ad5176a 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -130,6 +130,28 @@ func resourceDockerContainer() *schema.Resource { Set: resourceDockerPortsHash, }, + "hosts": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "host": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + Set: resourceDockerHostsHash, + }, + "env": &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -323,6 +345,21 @@ func resourceDockerPortsHash(v interface{}) int { return hashcode.String(buf.String()) } +func resourceDockerHostsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["ip"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + if v, ok := m["host"]; 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{}) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 814941bb..bf2bc9e2 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -67,6 +67,11 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err createOpts.Config.ExposedPorts = exposedPorts } + extraHosts := []string{} + if v, ok := d.GetOk("extra_hosts"); ok { + extraHosts = extraHostsSetToDockerExtraHosts(v.(*schema.Set)) + } + volumes := map[string]struct{}{} binds := []string{} volumesFrom := []string{} @@ -100,7 +105,9 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err if len(portBindings) != 0 { hostConfig.PortBindings = portBindings } - + if len(extraHosts) != 0 { + hostConfig.ExtraHosts = extraHosts + } if len(binds) != 0 { hostConfig.Binds = binds } @@ -312,6 +319,19 @@ func portSetToDockerPorts(ports *schema.Set) (map[dc.Port]struct{}, map[dc.Port] 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{} From 1c8e4c81e494e97366e735f6ca10626b38709500 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Thu, 3 Dec 2015 10:51:59 +0000 Subject: [PATCH 37/74] provider/docker: Inline ports and volumes schemas for consistency --- resource_docker_container.go | 122 ++++++++++++++++------------------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 6ad5176a..76b3e153 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -118,16 +118,69 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeSet, Optional: true, ForceNew: true, - Elem: getVolumesElem(), - Set: resourceDockerVolumesHash, + Elem: &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, + }, + }, + }, + Set: resourceDockerVolumesHash, }, "ports": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: true, - Elem: getPortsElem(), - Set: resourceDockerPortsHash, + Elem: &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, + }, + }, + }, + Set: resourceDockerPortsHash, }, "hosts": &schema.Schema{ @@ -263,67 +316,6 @@ func resourceDockerContainer() *schema.Resource { } } -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{}) From 3f3e9fab7da7b2558bf77241de4601c040b2dbfa Mon Sep 17 00:00:00 2001 From: stack72 Date: Mon, 21 Dec 2015 09:54:24 +0000 Subject: [PATCH 38/74] Fixing yet more gofmt errors with imports --- resource_docker_container.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 242462e1..32385049 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -4,9 +4,10 @@ import ( "bytes" "fmt" + "regexp" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" - "regexp" ) func resourceDockerContainer() *schema.Resource { From a44b8739925eb979b863ca96ecf32337cd382f42 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Fri, 1 Jan 2016 09:57:21 +0100 Subject: [PATCH 39/74] Add network_mode support to docker --- resource_docker_container.go | 6 ++++++ resource_docker_container_funcs.go | 4 ++++ resource_docker_container_test.go | 1 + 3 files changed, 11 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 32385049..ea73ca4f 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -238,6 +238,12 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, }, + + "network_mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 814941bb..110d5cc8 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -136,6 +136,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{})) } + if v, ok := d.GetOk("network_mode"); ok { + hostConfig.NetworkMode = v + } + createOpts.HostConfig = hostConfig var retContainer *dc.Container diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index df8ba0cb..a5c36a5c 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -155,5 +155,6 @@ resource "docker_container" "foo" { max-size = "10m" max-file = 20 } + network_mode = "bridge" } ` From 1d60a86a14c4c842e3d2135daa4fac7f17d7e0d4 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Fri, 1 Jan 2016 10:12:43 +0100 Subject: [PATCH 40/74] Convert v to string --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 110d5cc8..5be5091e 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -137,7 +137,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } if v, ok := d.GetOk("network_mode"); ok { - hostConfig.NetworkMode = v + hostConfig.NetworkMode = v.(string) } createOpts.HostConfig = hostConfig From 94b54c2a30cd2b6dfc1deabdba66c32208c3f4bc Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Sat, 2 Jan 2016 12:20:55 +0100 Subject: [PATCH 41/74] Add support of custom networks in docker --- provider.go | 1 + resource_docker_network.go | 135 +++++++++++++++++++++++++++++++ resource_docker_network_funcs.go | 115 ++++++++++++++++++++++++++ resource_docker_network_test.go | 65 +++++++++++++++ 4 files changed, 316 insertions(+) create mode 100644 resource_docker_network.go create mode 100644 resource_docker_network_funcs.go create mode 100644 resource_docker_network_test.go diff --git a/provider.go b/provider.go index fdc8b771..799fd9bd 100644 --- a/provider.go +++ b/provider.go @@ -28,6 +28,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "docker_container": resourceDockerContainer(), "docker_image": resourceDockerImage(), + "docker_network": resourceDockerNetwork(), }, ConfigureFunc: providerConfigure, diff --git a/resource_docker_network.go b/resource_docker_network.go new file mode 100644 index 00000000..4c14b2de --- /dev/null +++ b/resource_docker_network.go @@ -0,0 +1,135 @@ +package docker + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDockerNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceDockerNetworkCreate, + Read: resourceDockerNetworkRead, + Delete: resourceDockerNetworkDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "check_duplicate": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "driver": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "options": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "ipam_driver": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ipam_config": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: getIpamConfigElem(), + Set: resourceDockerIpamConfigHash, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "scope": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func getIpamConfigElem() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ip_range": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "gateway": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "aux_address": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceDockerIpamConfigHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["subnet"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + if v, ok := m["ip_range"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + if v, ok := m["gateway"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + if v, ok := m["aux_address"]; ok { + auxAddress := v.(map[string]interface{}) + + keys := make([]string, len(auxAddress)) + i := 0 + for k, _ := range auxAddress { + keys[i] = k + i++ + } + sort.Strings(keys) + + for _, k := range keys { + buf.WriteString(fmt.Sprintf("%v-%v-", k, auxAddress[k].(string))) + } + } + + return hashcode.String(buf.String()) +} diff --git a/resource_docker_network_funcs.go b/resource_docker_network_funcs.go new file mode 100644 index 00000000..61954f4a --- /dev/null +++ b/resource_docker_network_funcs.go @@ -0,0 +1,115 @@ +package docker + +import ( + "fmt" + + dc "github.com/fsouza/go-dockerclient" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + createOpts := dc.CreateNetworkOptions{ + Name: d.Get("name").(string), + } + if v, ok := d.GetOk("check_duplicate"); ok { + createOpts.CheckDuplicate = v.(bool) + } + if v, ok := d.GetOk("driver"); ok { + createOpts.Driver = v.(string) + } + if v, ok := d.GetOk("options"); ok { + createOpts.Options = v.(map[string]interface{}) + } + + ipamOpts := dc.IPAMOptions{} + ipamOptsSet := false + if v, ok := d.GetOk("ipam_driver"); ok { + ipamOpts.Driver = v.(string) + ipamOptsSet = true + } + if v, ok := d.GetOk("ipam_config"); ok { + ipamOpts.Config = ipamConfigSetToIpamConfigs(v.(*schema.Set)) + ipamOptsSet = true + } + + if ipamOptsSet { + createOpts.IPAM = ipamOpts + } + + var err error + var retNetwork *dc.Network + if retNetwork, err = client.CreateNetwork(createOpts); err != nil { + return fmt.Errorf("Unable to create network: %s", err) + } + if retNetwork == nil { + return fmt.Errorf("Returned network is nil") + } + + d.SetId(retNetwork.ID) + d.Set("name", retNetwork.Name) + d.Set("scope", retNetwork.Scope) + d.Set("driver", retNetwork.Driver) + d.Set("options", retNetwork.Options) + + return nil +} + +func resourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + var err error + var retNetwork *dc.Network + if retNetwork, err = client.NetworkInfo(d.Id()); err != nil { + if _, ok := err.(*dc.NoSuchNetwork); !ok { + return fmt.Errorf("Unable to inspect network: %s", err) + } + } + if retNetwork == nil { + d.SetId("") + return nil + } + + d.Set("scope", retNetwork.Scope) + d.Set("driver", retNetwork.Driver) + d.Set("options", retNetwork.Options) + + return nil +} + +func resourceDockerNetworkDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + if err := client.RemoveNetwork(d.Id()); err != nil { + if _, ok := err.(*dc.NoSuchNetwork); !ok { + return fmt.Errorf("Error deleting network %s: %s", d.Id(), err) + } + } + + d.SetId("") + return nil +} + +func ipamConfigSetToIpamConfigs(ipamConfigSet *schema.Set) []dc.IPAMConfig { + ipamConfigs := make([]dc.IPAMConfig, ipamConfigSet.Len()) + + for i, ipamConfigInt := range ipamConfigSet.List() { + ipamConfigRaw := ipamConfigInt.(map[string]interface{}) + + ipamConfig := dc.IPAMConfig{} + ipamConfig.Subnet = ipamConfigRaw["subnet"].(string) + ipamConfig.IPRange = ipamConfigRaw["ip_range"].(string) + ipamConfig.Gateway = ipamConfigRaw["gateway"].(string) + + auxAddressRaw := ipamConfigRaw["aux_address"].(map[string]interface{}) + ipamConfig.AuxAddress = make(map[string]string, len(auxAddressRaw)) + for k, v := range auxAddressRaw { + ipamConfig.AuxAddress[k] = v.(string) + } + + ipamConfigs[i] = ipamConfig + } + + return ipamConfigs +} diff --git a/resource_docker_network_test.go b/resource_docker_network_test.go new file mode 100644 index 00000000..6e3bb4e3 --- /dev/null +++ b/resource_docker_network_test.go @@ -0,0 +1,65 @@ +package docker + +import ( + "fmt" + "testing" + + dc "github.com/fsouza/go-dockerclient" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDockerNetwork_basic(t *testing.T) { + var n dc.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerNetworkConfig, + Check: resource.ComposeTestCheckFunc( + testAccNetwork("docker_network.foo", &n), + ), + }, + }, + }) +} + +func testAccNetwork(n string, network *dc.Network) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*dc.Client) + networks, err := client.ListNetworks() + if err != nil { + return err + } + + for _, n := range networks { + if n.ID == rs.Primary.ID { + inspected, err := client.NetworkInfo(n.ID) + if err != nil { + return fmt.Errorf("Network could not be obtained: %s", err) + } + *network = *inspected + return nil + } + } + + return fmt.Errorf("Network not found: %s", rs.Primary.ID) + } +} + +const testAccDockerNetworkConfig = ` +resource "docker_network" "foo" { + name = "bar" +} +` From 99f83853f75337d75eebd413ca5acce8c8a40345 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Mon, 4 Jan 2016 20:58:54 +0100 Subject: [PATCH 42/74] Add the networks entry --- resource_docker_container.go | 6 ++++++ resource_docker_container_funcs.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 32385049..f20ff43f 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -238,6 +238,12 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, }, + + "networks": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 814941bb..605db710 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -148,6 +148,14 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) + if v, ok := d.GetOk("networks"); ok { + connectionOpts := &dc.NetworkConnectionOptions{Container: retContainer.ID} + + for _, network := range v.(*schema.Set).List() { + client.ConnectNetwork(network.(string), connectionOpts) + } + } + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { return fmt.Errorf("Unable to start container: %s", err) From c823565d5d79f7348a3bdd00e3e46832d59f7f2a Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Mon, 4 Jan 2016 21:03:53 +0100 Subject: [PATCH 43/74] Fix typo --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 605db710..08cfe190 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -149,7 +149,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) if v, ok := d.GetOk("networks"); ok { - connectionOpts := &dc.NetworkConnectionOptions{Container: retContainer.ID} + connectionOpts := dc.NetworkConnectionOptions{Container: retContainer.ID} for _, network := range v.(*schema.Set).List() { client.ConnectNetwork(network.(string), connectionOpts) From bca87ec0ffb42cb187031b9b48b11393e9816b39 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Tue, 5 Jan 2016 03:46:24 +0100 Subject: [PATCH 44/74] Add Elem and Set to the network set --- resource_docker_container.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index f20ff43f..6e5bc07d 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -243,6 +243,8 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeSet, Optional: true, ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: stringSetHash, }, }, } From ed3a43a2fe0027635a6a635fddc027ad75eb7fef Mon Sep 17 00:00:00 2001 From: James Nugent Date: Thu, 14 Jan 2016 09:12:05 +0000 Subject: [PATCH 45/74] provider/docker: Fix flaky integration tests Asserting on the value of `latest` on an image is prone to failing because of new images being pushed upstream. Instead of asserting on a hash, we assert that the value matches a regular expression for the format of an image hash. --- resource_docker_image_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 3a1c5b13..81c50874 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -1,6 +1,7 @@ package docker import ( + "regexp" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -14,17 +15,14 @@ func TestAccDockerImage_basic(t *testing.T) { resource.TestStep{ Config: testAccDockerImageConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "docker_image.foo", - "latest", - "8dd8107abd2e22bfd3b45b05733f3d2677d4078b09b5edce56ee3d8677d3c648"), + resource.TestMatchResourceAttr("docker_image.foo", "latest", regexp.MustCompile(`\A[a-f0-9]{64}\z`)), ), }, }, }) } -func TestAddDockerImage_private(t *testing.T) { +func TestAccDockerImage_private(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -32,10 +30,7 @@ func TestAddDockerImage_private(t *testing.T) { resource.TestStep{ Config: testAddDockerPrivateImageConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "docker_image.foobar", - "latest", - "2c40b0526b6358710fd09e7b8c022429268cc61703b4777e528ac9d469a07ca1"), + resource.TestMatchResourceAttr("docker_image.foobar", "latest", regexp.MustCompile(`\A[a-f0-9]{64}\z`)), ), }, }, From 75e1fee5fa433c48c52c8a0f6e830543565608d3 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Fri, 15 Jan 2016 02:59:07 +0000 Subject: [PATCH 46/74] provider/docker: Tweak and test `host_entry` This adds acceptance tests for specifying extra hosts on Docker containers. It also renames the repeating block from `hosts` to `host`, which reads more naturally in the schema when multiple instances of the block are declared. --- resource_docker_container.go | 2 +- resource_docker_container_funcs.go | 2 +- resource_docker_container_test.go | 51 ++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 3ee87cf7..c8f363e3 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -184,7 +184,7 @@ func resourceDockerContainer() *schema.Resource { Set: resourceDockerPortsHash, }, - "hosts": &schema.Schema{ + "host": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 1c3f3d1d..47878f93 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -68,7 +68,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } extraHosts := []string{} - if v, ok := d.GetOk("extra_hosts"); ok { + if v, ok := d.GetOk("host"); ok { extraHosts = extraHostsSetToDockerExtraHosts(v.(*schema.Set)) } diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index a5c36a5c..aa1eee96 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -72,6 +72,18 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container does not have the correct max-file log option: %v", c.HostConfig.LogConfig.Config["max-file"]) } + if len(c.HostConfig.ExtraHosts) != 2 { + return fmt.Errorf("Container does not have correct number of extra host entries, got %d", len(c.HostConfig.ExtraHosts)) + } + + if c.HostConfig.ExtraHosts[0] != "testhost2:10.0.2.0" { + return fmt.Errorf("Container has incorrect extra host string: %q", c.HostConfig.ExtraHosts[0]) + } + + if c.HostConfig.ExtraHosts[1] != "testhost:10.0.1.0" { + return fmt.Errorf("Container has incorrect extra host string: %q", c.HostConfig.ExtraHosts[1]) + } + return nil } @@ -132,6 +144,7 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" } ` + const testAccDockerContainerCustomizedConfig = ` resource "docker_image" "foo" { name = "nginx:latest" @@ -140,21 +153,31 @@ resource "docker_image" "foo" { resource "docker_container" "foo" { name = "tf-test" image = "${docker_image.foo.latest}" - entrypoint = ["/bin/bash", "-c", "ping localhost"] - restart = "on-failure" - max_retry_count = 5 - memory = 512 - memory_swap = 2048 - cpu_shares = 32 - labels { - env = "prod" - role = "test" - } - log_driver = "json-file" - log_opts = { - max-size = "10m" - max-file = 20 + entrypoint = ["/bin/bash", "-c", "ping localhost"] + restart = "on-failure" + max_retry_count = 5 + memory = 512 + memory_swap = 2048 + cpu_shares = 32 + labels { + env = "prod" + role = "test" + } + log_driver = "json-file" + log_opts = { + max-size = "10m" + max-file = 20 } network_mode = "bridge" + + host { + host = "testhost" + ip = "10.0.1.0" + } + + host { + host = "testhost2" + ip = "10.0.2.0" + } } ` From 8bf14ffeb7efc8b19c91c5de3b87586f0adcf0a8 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Fri, 15 Jan 2016 17:33:17 +0000 Subject: [PATCH 47/74] provider/docker: Add `docker_volume` resource --- provider.go | 1 + resource_docker_volume.go | 102 +++++++++++++++++++++++++++++++++ resource_docker_volume_test.go | 67 ++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 resource_docker_volume.go create mode 100644 resource_docker_volume_test.go diff --git a/provider.go b/provider.go index 799fd9bd..b842d6b6 100644 --- a/provider.go +++ b/provider.go @@ -29,6 +29,7 @@ func Provider() terraform.ResourceProvider { "docker_container": resourceDockerContainer(), "docker_image": resourceDockerImage(), "docker_network": resourceDockerNetwork(), + "docker_volume": resourceDockerVolume(), }, ConfigureFunc: providerConfigure, diff --git a/resource_docker_volume.go b/resource_docker_volume.go new file mode 100644 index 00000000..33c22d58 --- /dev/null +++ b/resource_docker_volume.go @@ -0,0 +1,102 @@ +package docker + +import ( + "fmt" + + dc "github.com/fsouza/go-dockerclient" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDockerVolume() *schema.Resource { + return &schema.Resource{ + Create: resourceDockerVolumeCreate, + Read: resourceDockerVolumeRead, + Delete: resourceDockerVolumeDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "driver": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "driver_opts": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + "mountpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceDockerVolumeCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + createOpts := dc.CreateVolumeOptions{} + if v, ok := d.GetOk("name"); ok { + createOpts.Name = v.(string) + } + if v, ok := d.GetOk("driver"); ok { + createOpts.Driver = v.(string) + } + if v, ok := d.GetOk("driver_opts"); ok { + createOpts.DriverOpts = mapTypeMapValsToString(v.(map[string]interface{})) + } + + var err error + var retVolume *dc.Volume + if retVolume, err = client.CreateVolume(createOpts); err != nil { + return fmt.Errorf("Unable to create volume: %s", err) + } + if retVolume == nil { + return fmt.Errorf("Returned volume is nil") + } + + d.SetId(retVolume.Name) + d.Set("name", retVolume.Name) + d.Set("driver", retVolume.Driver) + d.Set("mountpoint", retVolume.Mountpoint) + + return nil +} + +func resourceDockerVolumeRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + var err error + var retVolume *dc.Volume + if retVolume, err = client.InspectVolume(d.Id()); err != nil && err != dc.ErrNoSuchVolume { + return fmt.Errorf("Unable to inspect volume: %s", err) + } + if retVolume == nil { + d.SetId("") + return nil + } + + d.Set("name", retVolume.Name) + d.Set("driver", retVolume.Driver) + d.Set("mountpoint", retVolume.Mountpoint) + + return nil +} + +func resourceDockerVolumeDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + + if err := client.RemoveVolume(d.Id()); err != nil && err != dc.ErrNoSuchVolume { + return fmt.Errorf("Error deleting volume %s: %s", d.Id(), err) + } + + d.SetId("") + return nil +} diff --git a/resource_docker_volume_test.go b/resource_docker_volume_test.go new file mode 100644 index 00000000..38fec3c4 --- /dev/null +++ b/resource_docker_volume_test.go @@ -0,0 +1,67 @@ +package docker + +import ( + "fmt" + "testing" + + dc "github.com/fsouza/go-dockerclient" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDockerVolume_basic(t *testing.T) { + var v dc.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerVolumeConfig, + Check: resource.ComposeTestCheckFunc( + checkDockerVolume("docker_volume.foo", &v), + resource.TestCheckResourceAttr("docker_volume.foo", "id", "testAccDockerVolume_basic"), + resource.TestCheckResourceAttr("docker_volume.foo", "name", "testAccDockerVolume_basic"), + ), + }, + }, + }) +} + +func checkDockerVolume(n string, volume *dc.Volume) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*dc.Client) + volumes, err := client.ListVolumes(dc.ListVolumesOptions{}) + if err != nil { + return err + } + + for _, v := range volumes { + if v.Name == rs.Primary.ID { + inspected, err := client.InspectVolume(v.Name) + if err != nil { + return fmt.Errorf("Volume could not be inspected: %s", err) + } + *volume = *inspected + return nil + } + } + + return fmt.Errorf("Volume not found: %s", rs.Primary.ID) + } +} + +const testAccDockerVolumeConfig = ` +resource "docker_volume" "foo" { + name = "testAccDockerVolume_basic" +} +` From 50d8d5773f1113b6bbfe9147325c4291fe9c902e Mon Sep 17 00:00:00 2001 From: James Nugent Date: Fri, 15 Jan 2016 21:59:33 +0000 Subject: [PATCH 48/74] provider/docker: Mount named volumes in containers This adds support for specifying named volumes for mounting in a `docker_container` resource. --- resource_docker_container.go | 18 +++++++++ resource_docker_container_funcs.go | 9 +++-- resource_docker_container_test.go | 63 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index c8f363e3..3cff902a 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -137,6 +137,20 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + if !regexp.MustCompile(`^/`).MatchString(value) { + es = append(es, fmt.Errorf( + "%q must be an absolute path", k)) + } + return + }, + }, + + "volume_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, }, "read_only": &schema.Schema{ @@ -383,6 +397,10 @@ func resourceDockerVolumesHash(v interface{}) int { buf.WriteString(fmt.Sprintf("%v-", v.(string))) } + if v, ok := m["volume_name"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + if v, ok := m["read_only"]; ok { buf.WriteString(fmt.Sprintf("%v-", v.(bool))) } diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 47878f93..39d2c09f 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -353,7 +353,10 @@ func volumeSetToDockerVolumes(volumes *schema.Set) (map[string]struct{}, []strin volume := volumeInt.(map[string]interface{}) fromContainer := volume["from_container"].(string) containerPath := volume["container_path"].(string) - hostPath := volume["host_path"].(string) + volumeName := volume["volume_name"].(string) + if len(volumeName) == 0 { + volumeName = volume["host_path"].(string) + } readOnly := volume["read_only"].(bool) switch { @@ -363,13 +366,13 @@ func volumeSetToDockerVolumes(volumes *schema.Set) (map[string]struct{}, []strin 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: + case len(volumeName) != 0: readWrite := "rw" if readOnly { readWrite = "ro" } retVolumeMap[containerPath] = struct{}{} - retHostConfigBinds = append(retHostConfigBinds, hostPath+":"+containerPath+":"+readWrite) + retHostConfigBinds = append(retHostConfigBinds, volumeName+":"+containerPath+":"+readWrite) default: retVolumeMap[containerPath] = struct{}{} } diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index aa1eee96..8536c78c 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -25,6 +25,48 @@ func TestAccDockerContainer_basic(t *testing.T) { }) } +func TestAccDockerContainer_volume(t *testing.T) { + var c dc.Container + + testCheck := func(*terraform.State) error { + if len(c.Mounts) != 2 { + return fmt.Errorf("Incorrect number of mounts: expected 2, got %d", len(c.Mounts)) + } + + for _, v := range c.Mounts { + if v.Name != "testAccDockerContainerVolume_volume" { + continue + } + + if v.Destination != "/tmp/volume" { + return fmt.Errorf("Bad destination on mount: expected /tmp/volume, got %q", v.Destination) + } + + if v.Mode != "rw" { + return fmt.Errorf("Bad mode on mount: expected rw, got %q", v.Mode) + } + + return nil + } + + return fmt.Errorf("Mount for testAccDockerContainerVolume_volume not found") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerVolumeConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + ), + }, + }, + }) +} + func TestAccDockerContainer_customized(t *testing.T) { var c dc.Container @@ -145,6 +187,27 @@ resource "docker_container" "foo" { } ` +const testAccDockerContainerVolumeConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" +} + +resource "docker_volume" "foo" { + name = "testAccDockerContainerVolume_volume" +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + volumes { + volume_name = "${docker_volume.foo.name}" + container_path = "/tmp/volume" + read_only = false + } +} +` + const testAccDockerContainerCustomizedConfig = ` resource "docker_image" "foo" { name = "nginx:latest" From edd907c0434ef2529e609f4365c7c358db6aa0a0 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Sun, 31 Jan 2016 08:31:30 +1100 Subject: [PATCH 49/74] Catch potential custom network errors in docker --- resource_docker_container_funcs.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 39d2c09f..44cd07f3 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -162,8 +162,11 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err if v, ok := d.GetOk("networks"); ok { connectionOpts := dc.NetworkConnectionOptions{Container: retContainer.ID} - for _, network := range v.(*schema.Set).List() { - client.ConnectNetwork(network.(string), connectionOpts) + 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) + } } } From f763cd8b35588884f4408d87d0b7d78c6d179bd6 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Sun, 31 Jan 2016 08:49:35 +1100 Subject: [PATCH 50/74] remove extra parenthesis --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 44cd07f3..0f276b0f 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -164,7 +164,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err for _, rawNetwork := range v.(*schema.Set).List() { network := rawNetwork.(string) - if err := client.ConnectNetwork(network), connectionOpts); err != nil { + if err := client.ConnectNetwork(network, connectionOpts); err != nil { return fmt.Errorf("Unable to connect to network '%s': %s", network, err) } } From 5e0b6e37509e27fcf36220e17034ec7f27adaa6e Mon Sep 17 00:00:00 2001 From: Trevor Pounds Date: Sun, 7 Feb 2016 15:51:26 -0800 Subject: [PATCH 51/74] Use built-in schema.HashString. --- resource_docker_container.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 3cff902a..28e14230 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -85,7 +85,7 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: stringSetHash, + Set: schema.HashString, }, "publish_all_ports": &schema.Schema{ @@ -225,7 +225,7 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: stringSetHash, + Set: schema.HashString, }, "links": &schema.Schema{ @@ -233,7 +233,7 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: stringSetHash, + Set: schema.HashString, }, "ip_address": &schema.Schema{ @@ -339,7 +339,7 @@ func resourceDockerContainer() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: stringSetHash, + Set: schema.HashString, }, }, } @@ -407,7 +407,3 @@ func resourceDockerVolumesHash(v interface{}) int { return hashcode.String(buf.String()) } - -func stringSetHash(v interface{}) int { - return hashcode.String(v.(string)) -} From eab6478caf6a1908648fb3582903cc728e5c463b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Steenis Date: Wed, 10 Feb 2016 20:21:17 +0100 Subject: [PATCH 52/74] Change default DOCKER_HOST value, fixes #4923 --- provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider.go b/provider.go index b842d6b6..cc25e143 100644 --- a/provider.go +++ b/provider.go @@ -13,7 +13,7 @@ func Provider() terraform.ResourceProvider { "host": &schema.Schema{ Type: schema.TypeString, Required: true, - DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:/run/docker.sock"), + DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:///var/run/docker.sock"), Description: "The Docker daemon address", }, From 292c493762457004b058e21d263c389ef0e8a2cc Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Sat, 13 Feb 2016 22:04:51 +1100 Subject: [PATCH 53/74] Stop providing the hostConfig while starting the container --- resource_docker_container_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 0f276b0f..0453b06a 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -171,7 +171,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } creationTime = time.Now() - if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { + if err := client.StartContainer(retContainer.ID, nil); err != nil { return fmt.Errorf("Unable to start container: %s", err) } From 193c75b1642a1ac6486e6a6eca5227769204d620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Verdo=C3=AFa=20Laurent?= Date: Fri, 26 Feb 2016 12:38:31 +0900 Subject: [PATCH 54/74] provider/docker: #2417 Add support for restart policy unless-stopped --- resource_docker_container.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 28e14230..74e75882 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -101,9 +101,9 @@ func resourceDockerContainer() *schema.Resource { Default: "no", ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { value := v.(string) - if !regexp.MustCompile(`^(no|on-failure|always)$`).MatchString(value) { + if !regexp.MustCompile(`^(no|on-failure|always|unless-stopped)$`).MatchString(value) { es = append(es, fmt.Errorf( - "%q must be one of \"no\", \"on-failure\", or \"always\"", k)) + "%q must be one of \"no\", \"on-failure\", \"always\" or \"unless-stopped\"", k)) } return }, From f7936ad8b3ce5cd16c6d825d7c4998de4db0d43a Mon Sep 17 00:00:00 2001 From: Raphael Randschau Date: Sun, 6 Mar 2016 16:22:11 +0100 Subject: [PATCH 55/74] Fix docker test assertions regarding latest tag --- resource_docker_image_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 81c50874..67c5317c 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/terraform/helper/resource" ) +var contentDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) + func TestAccDockerImage_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -15,7 +17,7 @@ func TestAccDockerImage_basic(t *testing.T) { resource.TestStep{ Config: testAccDockerImageConfig, Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr("docker_image.foo", "latest", regexp.MustCompile(`\A[a-f0-9]{64}\z`)), + resource.TestMatchResourceAttr("docker_image.foo", "latest", contentDigestRegexp), ), }, }, @@ -30,7 +32,7 @@ func TestAccDockerImage_private(t *testing.T) { resource.TestStep{ Config: testAddDockerPrivateImageConfig, Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr("docker_image.foobar", "latest", regexp.MustCompile(`\A[a-f0-9]{64}\z`)), + resource.TestMatchResourceAttr("docker_image.foobar", "latest", contentDigestRegexp), ), }, }, From 91cfbf9b4e7c06688b15869828b8a42b7cd0763b Mon Sep 17 00:00:00 2001 From: Rhyas Date: Tue, 22 Mar 2016 22:56:51 -0600 Subject: [PATCH 56/74] Fix Image Destroy bug. #3609 #3771 --- resource_docker_image_funcs.go | 54 ++++++++++++++++++++++++++-------- resource_docker_image_test.go | 32 +++++++++++++++++--- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index 454113c5..cccb37dc 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -41,10 +41,49 @@ func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error { } func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + err := removeImage(d, client) + if err != nil { + return fmt.Errorf("Unable to remove Docker image: %s", err) + } d.SetId("") return nil } +func searchLocalImages(data Data, imageName string) *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 +} + +func removeImage(d *schema.ResourceData, client *dc.Client) error { + var data Data + if err := fetchLocalImages(&data, client); err != nil { + return err + } + + imageName := d.Get("name").(string) + if imageName == "" { + return fmt.Errorf("Empty image name is not allowed") + } + + foundImage := searchLocalImages(data, imageName) + + if foundImage != nil { + err := client.RemoveImage(foundImage.ID) + if err != nil { + return err + } + } + + return nil +} + func fetchLocalImages(data *Data, client *dc.Client) error { images, err := client.ListImages(dc.ListImagesOptions{All: false}) if err != nil { @@ -146,18 +185,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) 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() + foundImage := searchLocalImages(data, imageName) if d.Get("keep_updated").(bool) || foundImage == nil { if err := pullImage(&data, client, imageName); err != nil { @@ -165,7 +193,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) } } - foundImage = searchLocal() + foundImage = searchLocalImages(data, imageName) if foundImage != nil { return foundImage, nil } diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 67c5317c..23e535f9 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -1,18 +1,22 @@ package docker import ( + "fmt" "regexp" "testing" + dc "github.com/fsouza/go-dockerclient" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) var contentDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) func TestAccDockerImage_basic(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccDockerImageDestroy, Steps: []resource.TestStep{ resource.TestStep{ Config: testAccDockerImageConfig, @@ -26,8 +30,9 @@ func TestAccDockerImage_basic(t *testing.T) { func TestAccDockerImage_private(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccDockerImageDestroy, Steps: []resource.TestStep{ resource.TestStep{ Config: testAddDockerPrivateImageConfig, @@ -39,6 +44,25 @@ func TestAccDockerImage_private(t *testing.T) { }) } +func testAccDockerImageDestroy(s *terraform.State) error { + //client := testAccProvider.Meta().(*dc.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "docker_image" { + continue + } + + client := testAccProvider.Meta().(*dc.Client) + _, err := client.InspectImage(rs.Primary.Attributes["latest"]) + if err == nil { + return fmt.Errorf("Image still exists") + } else if err != dc.ErrNoSuchImage { + return err + } + } + return nil +} + const testAccDockerImageConfig = ` resource "docker_image" "foo" { name = "alpine:3.1" From 5cdb31fec172c3abd8f1a88b608b38ba053c8e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VERDO=C3=8FA=20Laurent?= Date: Tue, 5 Apr 2016 11:43:59 +0900 Subject: [PATCH 57/74] provider/docker: #5298 Add support for docker run --user option --- resource_docker_container.go | 7 +++++++ resource_docker_container_funcs.go | 4 ++++ resource_docker_container_test.go | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 74e75882..3bcf9223 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -80,6 +80,13 @@ func resourceDockerContainer() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, + "user": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dns": &schema.Schema{ Type: schema.TypeSet, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 0453b06a..00090294 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -57,6 +57,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err 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{} diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 8536c78c..9e9158c4 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -78,6 +78,10 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container wrong entrypoint: %s", c.Config.Entrypoint) } + if c.Config.User != "root:root" { + return fmt.Errorf("Container wrong user: %s", c.Config.User) + } + if c.HostConfig.RestartPolicy.Name == "on-failure" { if c.HostConfig.RestartPolicy.MaximumRetryCount != 5 { return fmt.Errorf("Container has wrong restart policy max retry count: %d", c.HostConfig.RestartPolicy.MaximumRetryCount) @@ -217,6 +221,7 @@ resource "docker_container" "foo" { name = "tf-test" image = "${docker_image.foo.latest}" entrypoint = ["/bin/bash", "-c", "ping localhost"] + user = "root:root" restart = "on-failure" max_retry_count = 5 memory = 512 From c943ed4b789a598ababb055808b245cc230f5f5d Mon Sep 17 00:00:00 2001 From: Xavier Sellier Date: Wed, 27 Apr 2016 12:18:02 -0400 Subject: [PATCH 58/74] Provider Docker: (#6376) - Add option keep_locally - Add unit test - Add documentation --- resource_docker_image.go | 5 +++++ resource_docker_image_funcs.go | 5 +++++ resource_docker_image_test.go | 38 ++++++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/resource_docker_image.go b/resource_docker_image.go index 54822d73..09b6d32b 100644 --- a/resource_docker_image.go +++ b/resource_docker_image.go @@ -26,6 +26,11 @@ func resourceDockerImage() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "keep_locally": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, }, } } diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index cccb37dc..03d287c0 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -63,6 +63,11 @@ func searchLocalImages(data Data, imageName string) *dc.APIImages { func removeImage(d *schema.ResourceData, client *dc.Client) error { var data Data + + if keepLocally := d.Get("keep_locally").(bool); keepLocally { + return nil + } + if err := fetchLocalImages(&data, client); err != nil { return err } diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 23e535f9..adf35fd9 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -44,9 +44,36 @@ func TestAccDockerImage_private(t *testing.T) { }) } -func testAccDockerImageDestroy(s *terraform.State) error { - //client := testAccProvider.Meta().(*dc.Client) +func TestAccDockerImage_destroy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "docker_image" { + continue + } + client := testAccProvider.Meta().(*dc.Client) + _, err := client.InspectImage(rs.Primary.Attributes["latest"]) + if err != nil { + return err + } + } + return nil + }, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageKeepLocallyConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("docker_image.foobarzoo", "latest", contentDigestRegexp), + ), + }, + }, + }) +} + +func testAccDockerImageDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "docker_image" { continue @@ -76,3 +103,10 @@ resource "docker_image" "foobar" { keep_updated = true } ` + +const testAccDockerImageKeepLocallyConfig = ` +resource "docker_image" "foobarzoo" { + name = "crux:3.1" + keep_locally = true +} +` From 4ec88d82abee849978d228968e01884068ed5014 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Fri, 29 Apr 2016 18:42:24 -0500 Subject: [PATCH 59/74] provider/docker: don't crash with empty commands 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. --- resource_docker_container_funcs.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 00090294..b475884b 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -51,6 +51,11 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err 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 { @@ -269,6 +274,10 @@ func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) err 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 From ae92cc7e30e7ef0c87595b7b1bd026d4b5e5086b Mon Sep 17 00:00:00 2001 From: stack72 Date: Wed, 29 Jun 2016 09:38:56 +0100 Subject: [PATCH 60/74] provider/docker: Fixing the Docker Container Mount Test ``` make testacc TEST=./builtin/providers/docker TESTARGS='-run=TestAccDockerContainer_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /vendor/) TF_ACC=1 go test ./builtin/providers/docker -v -run=TestAccDockerContainer_ -timeout 120m === RUN TestAccDockerContainer_basic --- PASS: TestAccDockerContainer_basic (17.25s) === RUN TestAccDockerContainer_volume --- PASS: TestAccDockerContainer_volume (16.79s) === RUN TestAccDockerContainer_customized --- PASS: TestAccDockerContainer_customized (19.65s) PASS ok github.com/hashicorp/terraform/builtin/providers/docker 53.712s ``` --- resource_docker_container_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 9e9158c4..89877358 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -29,8 +29,8 @@ func TestAccDockerContainer_volume(t *testing.T) { var c dc.Container testCheck := func(*terraform.State) error { - if len(c.Mounts) != 2 { - return fmt.Errorf("Incorrect number of mounts: expected 2, got %d", len(c.Mounts)) + if len(c.Mounts) != 1 { + return fmt.Errorf("Incorrect number of mounts: expected 1, got %d", len(c.Mounts)) } for _, v := range c.Mounts { From efcec57608d15a437fbc8727d74881b863e3756c Mon Sep 17 00:00:00 2001 From: Daniel Portella Date: Wed, 29 Jun 2016 13:38:46 +0100 Subject: [PATCH 61/74] provider/docker: Docker DNS Setting Enhancements (#7392) * fixed go vet issues on aws provider in master * added support for dns, dns options and dns search for docker container. On docker container resource you can specify dns_opts nad dns_search which maps directly to docker --dns_opt and --dns_search parameters. Allowing users to setup the embedded dns settings for their containers. * fixed the asserts for the new features in tests. fixed tests around DNS, DNS_OPTS and DNS_SEARCH --- resource_docker_container.go | 16 ++++++++++++++ resource_docker_container_funcs.go | 8 +++++++ resource_docker_container_test.go | 35 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 3bcf9223..604b116e 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -95,6 +95,22 @@ func resourceDockerContainer() *schema.Resource { Set: schema.HashString, }, + "dns_opts": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "dns_search": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "publish_all_ports": &schema.Schema{ Type: schema.TypeBool, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index b475884b..a2aa1479 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -128,6 +128,14 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.DNS = stringSetToStringSlice(v.(*schema.Set)) } + if v, ok := d.GetOk("dns_opts"); ok { + hostConfig.DNSOptions = stringSetToStringSlice(v.(*schema.Set)) + } + + if v, ok := d.GetOk("dns_search"); ok { + hostConfig.DNSSearch = stringSetToStringSlice(v.(*schema.Set)) + } + if v, ok := d.GetOk("links"); ok { hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) } diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 89877358..167ac5db 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -102,6 +102,38 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) } + if len(c.HostConfig.DNS) != 1 { + return fmt.Errorf("Container does not have the correct number of dns entries: %d", len(c.HostConfig.DNS)) + } + + if c.HostConfig.DNS[0] != "8.8.8.8" { + return fmt.Errorf("Container has wrong dns setting: %v", c.HostConfig.DNS[0]) + } + + if len(c.HostConfig.DNSOptions) != 1 { + return fmt.Errorf("Container does not have the correct number of dns option entries: %d", len(c.HostConfig.DNS)) + } + + if c.HostConfig.DNSOptions[0] != "rotate" { + return fmt.Errorf("Container has wrong dns option setting: %v", c.HostConfig.DNS[0]) + } + + if len(c.HostConfig.DNSSearch) != 1 { + return fmt.Errorf("Container does not have the correct number of dns search entries: %d", len(c.HostConfig.DNS)) + } + + if c.HostConfig.DNSSearch[0] != "example.com" { + return fmt.Errorf("Container has wrong dns search setting: %v", c.HostConfig.DNS[0]) + } + + if c.HostConfig.CPUShares != 32 { + return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) + } + + if c.HostConfig.CPUShares != 32 { + return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) + } + if c.Config.Labels["env"] != "prod" || c.Config.Labels["role"] != "test" { return fmt.Errorf("Container does not have the correct labels") } @@ -227,6 +259,9 @@ resource "docker_container" "foo" { memory = 512 memory_swap = 2048 cpu_shares = 32 + dns = ["8.8.8.8"] + dns_opts = ["rotate"] + dns_search = ["example.com"] labels { env = "prod" role = "test" From 6154fad93339a273527dea6f4765f2ac8c95cf36 Mon Sep 17 00:00:00 2001 From: Daniel Portella Date: Wed, 29 Jun 2016 15:48:15 +0100 Subject: [PATCH 62/74] provider/docker: Docker documentation and additional test message (#7412) * added additional error info for when memory swap assert fails. related to https://github.com/hashicorp/terraform/pull/7392 * updated docker_container documentation reflect recent changes to docker provider around tests, dns options and dns search support. * Grammar and punctuation changes Docker container documentation. * Spell checking, grammar and punctuation. Docker container documentation. * Markdown change sto docker container documentation --- resource_docker_container_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 167ac5db..a3d7e925 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -95,7 +95,7 @@ func TestAccDockerContainer_customized(t *testing.T) { } if c.HostConfig.MemorySwap != (2048 * 1024 * 1024) { - return fmt.Errorf("Container has wrong memory swap setting: %d", c.HostConfig.MemorySwap) + return fmt.Errorf("Container has wrong memory swap setting: %d\n\r\tPlease check that you machine supports memory swap (you can do that by running 'docker info' command).", c.HostConfig.MemorySwap) } if c.HostConfig.CPUShares != 32 { From 439a19426cd0efc5807e2dbdffc381dd22d779a5 Mon Sep 17 00:00:00 2001 From: JB Arsenault Date: Mon, 11 Jul 2016 11:03:02 -0400 Subject: [PATCH 63/74] Add `destroy_grace_seconds` option to stop container before delete (#7513) --- resource_docker_container.go | 5 +++++ resource_docker_container_funcs.go | 8 ++++++++ resource_docker_container_test.go | 1 + 3 files changed, 14 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 604b116e..4e61bc2a 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -285,6 +285,11 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "destroy_grace_seconds": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "labels": &schema.Schema{ Type: schema.TypeMap, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index a2aa1479..9668fd0a 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -265,6 +265,14 @@ func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) err func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*dc.Client) + // Stop the container before removing if destroy_grace_seconds is defined + if d.Get("destroy_grace_seconds").(int) > 0 { + var timeout = uint(d.Get("destroy_grace_seconds").(int)) + if err := client.StopContainer(d.Id(), timeout); err != nil { + return fmt.Errorf("Error stopping container %s: %s", d.Id(), err) + } + } + removeOpts := dc.RemoveContainerOptions{ ID: d.Id(), RemoveVolumes: true, diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index a3d7e925..1c4da8cd 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -255,6 +255,7 @@ resource "docker_container" "foo" { entrypoint = ["/bin/bash", "-c", "ping localhost"] user = "root:root" restart = "on-failure" + destroy_grace_seconds = 10 max_retry_count = 5 memory = 512 memory_swap = 2048 From 46356f2d02b20c9157d00087412111de06d64989 Mon Sep 17 00:00:00 2001 From: kyhavlov Date: Tue, 26 Jul 2016 11:18:38 -0400 Subject: [PATCH 64/74] provider/docker: Added docker_registry_image data source (#7000) --- data_source_docker_registry_image.go | 166 ++++++++++++++++++++++ data_source_docker_registry_image_test.go | 52 +++++++ provider.go | 4 + resource_docker_image.go | 11 +- resource_docker_image_funcs.go | 66 +++++---- resource_docker_image_test.go | 28 +++- 6 files changed, 286 insertions(+), 41 deletions(-) create mode 100644 data_source_docker_registry_image.go create mode 100644 data_source_docker_registry_image_test.go diff --git a/data_source_docker_registry_image.go b/data_source_docker_registry_image.go new file mode 100644 index 00000000..9898c8ac --- /dev/null +++ b/data_source_docker_registry_image.go @@ -0,0 +1,166 @@ +package docker + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceDockerRegistryImage() *schema.Resource { + return &schema.Resource{ + Read: dataSourceDockerRegistryImageRead, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "sha256_digest": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error { + pullOpts := parseImageOptions(d.Get("name").(string)) + + // Use the official Docker Hub if a registry isn't specified + if pullOpts.Registry == "" { + pullOpts.Registry = "registry.hub.docker.com" + } else { + // Otherwise, filter the registry name out of the repo name + pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1) + } + + // Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul' + if !strings.Contains(pullOpts.Repository, "/") { + pullOpts.Repository = "library/" + pullOpts.Repository + } + + if pullOpts.Tag == "" { + pullOpts.Tag = "latest" + } + + digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, "", "") + + if err != nil { + return fmt.Errorf("Got error when attempting to fetch image version from registry: %s", err) + } + + d.SetId(digest) + d.Set("sha256_digest", digest) + + return nil +} + +func getImageDigest(registry, image, tag, username, password string) (string, error) { + client := http.DefaultClient + + req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil) + + if err != nil { + return "", fmt.Errorf("Error creating registry request: %s", err) + } + + if username != "" { + req.SetBasicAuth(username, password) + } + + resp, err := client.Do(req) + + if err != nil { + return "", fmt.Errorf("Error during registry request: %s", err) + } + + switch resp.StatusCode { + // Basic auth was valid or not needed + case http.StatusOK: + return resp.Header.Get("Docker-Content-Digest"), nil + + // Either OAuth is required or the basic auth creds were invalid + case http.StatusUnauthorized: + if strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") { + auth := parseAuthHeader(resp.Header.Get("www-authenticate")) + params := url.Values{} + params.Set("service", auth["service"]) + params.Set("scope", auth["scope"]) + tokenRequest, err := http.NewRequest("GET", auth["realm"]+"?"+params.Encode(), nil) + + if err != nil { + return "", fmt.Errorf("Error creating registry request: %s", err) + } + + if username != "" { + tokenRequest.SetBasicAuth(username, password) + } + + tokenResponse, err := client.Do(tokenRequest) + + if err != nil { + return "", fmt.Errorf("Error during registry request: %s", err) + } + + if tokenResponse.StatusCode != http.StatusOK { + return "", fmt.Errorf("Got bad response from registry: " + tokenResponse.Status) + } + + body, err := ioutil.ReadAll(tokenResponse.Body) + if err != nil { + return "", fmt.Errorf("Error reading response body: %s", err) + } + + token := &TokenResponse{} + err = json.Unmarshal(body, token) + if err != nil { + return "", fmt.Errorf("Error parsing OAuth token response: %s", err) + } + + req.Header.Set("Authorization", "Bearer "+token.Token) + digestResponse, err := client.Do(req) + + if err != nil { + return "", fmt.Errorf("Error during registry request: %s", err) + } + + if digestResponse.StatusCode != http.StatusOK { + return "", fmt.Errorf("Got bad response from registry: " + digestResponse.Status) + } + + return digestResponse.Header.Get("Docker-Content-Digest"), nil + } else { + return "", fmt.Errorf("Bad credentials: " + resp.Status) + } + + // Some unexpected status was given, return an error + default: + return "", fmt.Errorf("Got bad response from registry: " + resp.Status) + } +} + +type TokenResponse struct { + Token string +} + +// Parses key/value pairs from a WWW-Authenticate header +func parseAuthHeader(header string) map[string]string { + parts := strings.SplitN(header, " ", 2) + parts = strings.Split(parts[1], ",") + opts := make(map[string]string) + + for _, part := range parts { + vals := strings.SplitN(part, "=", 2) + key := vals[0] + val := strings.Trim(vals[1], "\", ") + opts[key] = val + } + + return opts +} diff --git a/data_source_docker_registry_image_test.go b/data_source_docker_registry_image_test.go new file mode 100644 index 00000000..aa34b004 --- /dev/null +++ b/data_source_docker_registry_image_test.go @@ -0,0 +1,52 @@ +package docker + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +var registryDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) + +func TestAccDockerRegistryImage_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.docker_registry_image.foo", "sha256_digest", registryDigestRegexp), + ), + }, + }, + }) +} + +func TestAccDockerRegistryImage_private(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageDataSourcePrivateConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.docker_registry_image.bar", "sha256_digest", registryDigestRegexp), + ), + }, + }, + }) +} + +const testAccDockerImageDataSourceConfig = ` +data "docker_registry_image" "foo" { + name = "alpine:latest" +} +` + +const testAccDockerImageDataSourcePrivateConfig = ` +data "docker_registry_image" "bar" { + name = "gcr.io:443/google_containers/pause:0.8.0" +} +` diff --git a/provider.go b/provider.go index cc25e143..cee438ae 100644 --- a/provider.go +++ b/provider.go @@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider { "docker_volume": resourceDockerVolume(), }, + DataSourcesMap: map[string]*schema.Resource{ + "docker_registry_image": dataSourceDockerRegistryImage(), + }, + ConfigureFunc: providerConfigure, } } diff --git a/resource_docker_image.go b/resource_docker_image.go index 09b6d32b..9c2f84d4 100644 --- a/resource_docker_image.go +++ b/resource_docker_image.go @@ -17,11 +17,6 @@ func resourceDockerImage() *schema.Resource { Required: true, }, - "keep_updated": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - }, - "latest": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -31,6 +26,12 @@ func resourceDockerImage() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + + "pull_trigger": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index 03d287c0..72cbc8ea 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -22,6 +22,25 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error { } func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*dc.Client) + var data Data + if err := fetchLocalImages(&data, client); err != nil { + return fmt.Errorf("Error reading docker image list: %s", err) + } + foundImage := searchLocalImages(data, d.Get("name").(string)) + + if foundImage != nil { + d.Set("latest", foundImage.ID) + } else { + d.SetId("") + } + + 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 client := meta.(*dc.Client) apiImage, err := findImage(d, client) if err != nil { @@ -33,13 +52,6 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error { 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 { client := meta.(*dc.Client) err := removeImage(d, client) @@ -117,6 +129,17 @@ 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 := parseImageOptions(image) + auth := dc.AuthConfiguration{} + + if err := client.PullImage(pullOpts, auth); err != nil { + return fmt.Errorf("Error pulling image %s: %s\n", image, err) + } + + return fetchLocalImages(data, client) +} + +func parseImageOptions(image string) dc.PullImageOptions { pullOpts := dc.PullImageOptions{} splitImageName := strings.Split(image, ":") @@ -151,32 +174,7 @@ func pullImage(data *Data, client *dc.Client, image string) error { 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 "" + return pullOpts } func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) { @@ -192,7 +190,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) foundImage := searchLocalImages(data, imageName) - if d.Get("keep_updated").(bool) || foundImage == nil { + if foundImage == nil { if err := pullImage(&data, client, imageName); err != nil { return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err) } diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index adf35fd9..484c45e8 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -73,6 +73,22 @@ func TestAccDockerImage_destroy(t *testing.T) { }) } +func TestAccDockerImage_data(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageFromDataConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("docker_image.foobarbaz", "latest", contentDigestRegexp), + ), + }, + }, + }) +} + func testAccDockerImageDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "docker_image" { @@ -93,14 +109,12 @@ func testAccDockerImageDestroy(s *terraform.State) error { const testAccDockerImageConfig = ` resource "docker_image" "foo" { name = "alpine:3.1" - keep_updated = false } ` const testAddDockerPrivateImageConfig = ` resource "docker_image" "foobar" { name = "gcr.io:443/google_containers/pause:0.8.0" - keep_updated = true } ` @@ -110,3 +124,13 @@ resource "docker_image" "foobarzoo" { keep_locally = true } ` + +const testAccDockerImageFromDataConfig = ` +data "docker_registry_image" "foobarbaz" { + name = "alpine:3.1" +} +resource "docker_image" "foobarbaz" { + name = "${data.docker_registry_image.foobarbaz.name}" + pull_trigger = "${data.docker_registry_image.foobarbaz.sha256_digest}" +} +` From 1801efaf99ac2e54676f5f09bcb7b651684c4188 Mon Sep 17 00:00:00 2001 From: Daniel Portella Date: Thu, 27 Oct 2016 10:54:05 +0100 Subject: [PATCH 65/74] provider/docker: Fixes for docker_container host object and documentation (#9367) * Updated docker container documentation Feedback from ticket #9350 indicated that documentation was out of date renamed `hosts_entry` to `host` added correct type information to *Extra Hosts* section. Refs: 9350 * Fixes for docker_container host object Feedback from ticket #9350 updated codebase so it reflects the requirements from docker in regards to `host` which is `Required` and not optional. It now accurately reflects the docker requirements and the terraform documentation. Test results > Bear in mind the failure it is because my laptop doesnt support memory swap. So this test will always fail. Changing the Schema from `optional` to `required` made no difference to the tests. make testacc TEST=./builtin/providers/docker/ ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2016/10/14 15:04:40 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/docker/ -v -timeout 120m === RUN TestAccDockerRegistryImage_basic --- PASS: TestAccDockerRegistryImage_basic (4.57s) === RUN TestAccDockerRegistryImage_private --- PASS: TestAccDockerRegistryImage_private (6.22s) === RUN TestProvider --- PASS: TestProvider (0.00s) === RUN TestProvider_impl --- PASS: TestProvider_impl (0.00s) === RUN TestAccDockerContainer_basic --- PASS: TestAccDockerContainer_basic (7.16s) === RUN TestAccDockerContainer_volume --- PASS: TestAccDockerContainer_volume (7.37s) === RUN TestAccDockerContainer_customized --- FAIL: TestAccDockerContainer_customized (18.99s) testing.go:265: Step 0 error: Check failed: Check 2/2 error: Container has wrong memory swap setting: -1 Please check that you machine supports memory swap (you can do that by running 'docker info' command). === RUN TestAccDockerImage_basic --- PASS: TestAccDockerImage_basic (2.58s) === RUN TestAccDockerImage_private --- PASS: TestAccDockerImage_private (2.70s) === RUN TestAccDockerImage_destroy --- PASS: TestAccDockerImage_destroy (30.00s) === RUN TestAccDockerImage_data --- PASS: TestAccDockerImage_data (5.93s) === RUN TestAccDockerNetwork_basic --- PASS: TestAccDockerNetwork_basic (0.24s) === RUN TestAccDockerVolume_basic --- PASS: TestAccDockerVolume_basic (0.05s) FAIL exit status 1 FAIL github.com/hashicorp/terraform/builtin/providers/docker 85.816s Makefile:47: recipe for target 'testacc' failed make: *** [testacc] Error 1 Refs: 9350 --- resource_docker_container.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 4e61bc2a..01b2453a 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -229,13 +229,13 @@ func resourceDockerContainer() *schema.Resource { Schema: map[string]*schema.Schema{ "ip": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, ForceNew: true, }, "host": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, ForceNew: true, }, }, From bdd81081f66b58e420df24b6ca84e0842f0e75a8 Mon Sep 17 00:00:00 2001 From: Michael Kuzmin Date: Tue, 22 Nov 2016 13:18:09 +0100 Subject: [PATCH 66/74] provider/docker: authentication via values instead of files (#10151) * Docker authentication via values * Rename parameters to ca_material, cert_material, and key_material. Add environment variables. --- config.go | 28 ++++++++++++++++++++-------- provider.go | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 19918274..d3a6cb12 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package docker import ( + "fmt" "path/filepath" dc "github.com/fsouza/go-dockerclient" @@ -10,21 +11,32 @@ import ( // Docker API compatible host. type Config struct { Host string + Ca string + Cert string + Key string CertPath string } // NewClient() returns a new Docker client. func (c *Config) NewClient() (*dc.Client, error) { - // If there is no cert information, then just return the direct client - if c.CertPath == "" { - return dc.NewClient(c.Host) + if c.Ca != "" || c.Cert != "" || c.Key != "" { + if c.Ca == "" || c.Cert == "" || c.Key == "" { + return nil, fmt.Errorf("ca_material, cert_material, and key_material must be specified") + } + + return dc.NewTLSClientFromBytes(c.Host, []byte(c.Cert), []byte(c.Key), []byte(c.Ca)) } - // If there is cert information, load it and use it. - ca := filepath.Join(c.CertPath, "ca.pem") - cert := filepath.Join(c.CertPath, "cert.pem") - key := filepath.Join(c.CertPath, "key.pem") - return dc.NewTLSClient(c.Host, cert, key, ca) + if c.CertPath != "" { + // If there is cert information, load it and use it. + ca := filepath.Join(c.CertPath, "ca.pem") + cert := filepath.Join(c.CertPath, "cert.pem") + key := filepath.Join(c.CertPath, "key.pem") + return dc.NewTLSClient(c.Host, cert, key, ca) + } + + // If there is no cert information, then just return the direct client + return dc.NewClient(c.Host) } // Data ia structure for holding data that we fetch from Docker. diff --git a/provider.go b/provider.go index cee438ae..61486530 100644 --- a/provider.go +++ b/provider.go @@ -17,6 +17,28 @@ func Provider() terraform.ResourceProvider { Description: "The Docker daemon address", }, + "ca_material": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CA_MATERIAL", ""), + ConflictsWith: []string{"cert_path"}, + Description: "PEM-encoded content of Docker host CA certificate", + }, + "cert_material": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_MATERIAL", ""), + ConflictsWith: []string{"cert_path"}, + Description: "PEM-encoded content of Docker client certificate", + }, + "key_material": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""), + ConflictsWith: []string{"cert_path"}, + Description: "PEM-encoded content of Docker client private key", + }, + "cert_path": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -43,6 +65,9 @@ func Provider() terraform.ResourceProvider { func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ Host: d.Get("host").(string), + Ca: d.Get("ca_material").(string), + Cert: d.Get("cert_material").(string), + Key: d.Get("key_material").(string), CertPath: d.Get("cert_path").(string), } From 738e90507e7d5cd3b27b37878e91d6b2e60946c4 Mon Sep 17 00:00:00 2001 From: Michael Kuzmin Date: Mon, 5 Dec 2016 14:06:34 +0300 Subject: [PATCH 67/74] provider/docker: Upload files into container before first start (#9520) * Create uploads section for docker containers * Upload a single file, load its content from state --- resource_docker_container.go | 38 +++++++++++++++++ resource_docker_container_funcs.go | 35 ++++++++++++++++ resource_docker_container_test.go | 67 ++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 01b2453a..2fb4efc2 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -369,6 +369,29 @@ func resourceDockerContainer() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, + + "upload": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": &schema.Schema{ + Type: schema.TypeString, + Required: true, + // This is intentional. The container is mutated once, and never updated later. + // New configuration forces a new deployment, even with the same binaries. + ForceNew: true, + }, + "file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + Set: resourceDockerUploadHash, + }, }, } } @@ -435,3 +458,18 @@ func resourceDockerVolumesHash(v interface{}) int { return hashcode.String(buf.String()) } + +func resourceDockerUploadHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["content"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + if v, ok := m["file"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v.(string))) + } + + return hashcode.String(buf.String()) +} diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index 9668fd0a..f74264a7 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -1,6 +1,8 @@ package docker import ( + "archive/tar" + "bytes" "errors" "fmt" "strconv" @@ -187,6 +189,39 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } } + if v, ok := d.GetOk("upload"); ok { + for _, upload := range v.(*schema.Set).List() { + content := upload.(map[string]interface{})["content"].(string) + file := upload.(map[string]interface{})["file"].(string) + + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + hdr := &tar.Header{ + Name: file, + Mode: 0644, + Size: int64(len(content)), + } + if err := tw.WriteHeader(hdr); err != nil { + return fmt.Errorf("Error creating tar archive: %s", err) + } + if _, err := tw.Write([]byte(content)); err != nil { + return fmt.Errorf("Error creating tar archive: %s", err) + } + if err := tw.Close(); err != nil { + return fmt.Errorf("Error creating tar archive: %s", err) + } + + uploadOpts := dc.UploadToContainerOptions{ + InputStream: bytes.NewReader(buf.Bytes()), + Path: "/", + } + + if err := client.UploadToContainer(retContainer.ID, uploadOpts); err != nil { + return fmt.Errorf("Unable to upload volume content: %s", err) + } + } + } + creationTime = time.Now() if err := client.StartContainer(retContainer.ID, nil); err != nil { return fmt.Errorf("Unable to start container: %s", err) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 1c4da8cd..99f0ab3e 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -1,6 +1,8 @@ package docker import ( + "archive/tar" + "bytes" "fmt" "testing" @@ -180,6 +182,55 @@ func TestAccDockerContainer_customized(t *testing.T) { }) } +func TestAccDockerContainer_upload(t *testing.T) { + var c dc.Container + + testCheck := func(*terraform.State) error { + client := testAccProvider.Meta().(*dc.Client) + + buf := new(bytes.Buffer) + opts := dc.DownloadFromContainerOptions{ + OutputStream: buf, + Path: "/terraform/test.txt", + } + + if err := client.DownloadFromContainer(c.ID, opts); err != nil { + return fmt.Errorf("Unable to download a file from container: %s", err) + } + + r := bytes.NewReader(buf.Bytes()) + tr := tar.NewReader(r) + + if _, err := tr.Next(); err != nil { + return fmt.Errorf("Unable to read content of tar archive: %s", err) + } + + fbuf := new(bytes.Buffer) + fbuf.ReadFrom(tr) + content := fbuf.String() + + if content != "foo" { + return fmt.Errorf("file content is invalid") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerUploadConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + ), + }, + }, + }) +} + func testAccContainerRunning(n string, container *dc.Container) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -285,3 +336,19 @@ resource "docker_container" "foo" { } } ` + +const testAccDockerContainerUploadConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + upload { + content = "foo" + file = "/terraform/test.txt" + } +} +` From 78397b5a9afddb1094907743998a59ddc013d972 Mon Sep 17 00:00:00 2001 From: Michael Kuzmin Date: Sat, 17 Dec 2016 15:41:08 +0300 Subject: [PATCH 68/74] provider/docker: fix regression, cert_path stop working (#10754) (#10801) --- config.go | 4 ++++ provider.go | 27 ++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/config.go b/config.go index d3a6cb12..ad05d540 100644 --- a/config.go +++ b/config.go @@ -24,6 +24,10 @@ func (c *Config) NewClient() (*dc.Client, error) { return nil, fmt.Errorf("ca_material, cert_material, and key_material must be specified") } + if c.CertPath != "" { + return nil, fmt.Errorf("cert_path must not be specified") + } + return dc.NewTLSClientFromBytes(c.Host, []byte(c.Cert), []byte(c.Key), []byte(c.Ca)) } diff --git a/provider.go b/provider.go index 61486530..1da7ffbe 100644 --- a/provider.go +++ b/provider.go @@ -18,25 +18,22 @@ func Provider() terraform.ResourceProvider { }, "ca_material": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("DOCKER_CA_MATERIAL", ""), - ConflictsWith: []string{"cert_path"}, - Description: "PEM-encoded content of Docker host CA certificate", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CA_MATERIAL", ""), + Description: "PEM-encoded content of Docker host CA certificate", }, "cert_material": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_MATERIAL", ""), - ConflictsWith: []string{"cert_path"}, - Description: "PEM-encoded content of Docker client certificate", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_MATERIAL", ""), + Description: "PEM-encoded content of Docker client certificate", }, "key_material": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""), - ConflictsWith: []string{"cert_path"}, - Description: "PEM-encoded content of Docker client private key", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""), + Description: "PEM-encoded content of Docker client private key", }, "cert_path": &schema.Schema{ From 8fcb881db30070fbc1bede71ed980e7f4d8a6fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VERDO=C3=8FA=20Laurent?= Date: Tue, 3 Jan 2017 12:46:16 +0100 Subject: [PATCH 69/74] provider/docker: Add network create --internal flag support (#10932) * provider/docker: Add network create --internal flag support * provider/docker: Add acceptance tests for network --internal flag --- resource_docker_network.go | 7 +++++++ resource_docker_network_funcs.go | 7 +++++++ resource_docker_network_test.go | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/resource_docker_network.go b/resource_docker_network.go index 4c14b2de..7279d2ee 100644 --- a/resource_docker_network.go +++ b/resource_docker_network.go @@ -42,6 +42,13 @@ func resourceDockerNetwork() *schema.Resource { Computed: true, }, + "internal": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "ipam_driver": &schema.Schema{ Type: schema.TypeString, Optional: true, diff --git a/resource_docker_network_funcs.go b/resource_docker_network_funcs.go index 61954f4a..f5ff172b 100644 --- a/resource_docker_network_funcs.go +++ b/resource_docker_network_funcs.go @@ -22,6 +22,9 @@ func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error if v, ok := d.GetOk("options"); ok { createOpts.Options = v.(map[string]interface{}) } + if v, ok := d.GetOk("internal"); ok { + createOpts.Internal = v.(bool) + } ipamOpts := dc.IPAMOptions{} ipamOptsSet := false @@ -53,6 +56,9 @@ func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error d.Set("driver", retNetwork.Driver) d.Set("options", retNetwork.Options) + // The 'internal' property is not send back when create network + d.Set("internal", createOpts.Internal) + return nil } @@ -74,6 +80,7 @@ func resourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error { d.Set("scope", retNetwork.Scope) d.Set("driver", retNetwork.Driver) d.Set("options", retNetwork.Options) + d.Set("internal", retNetwork.Internal) return nil } diff --git a/resource_docker_network_test.go b/resource_docker_network_test.go index 6e3bb4e3..5fe7f8b3 100644 --- a/resource_docker_network_test.go +++ b/resource_docker_network_test.go @@ -63,3 +63,37 @@ resource "docker_network" "foo" { name = "bar" } ` + +func TestAccDockerNetwork_internal(t *testing.T) { + var n dc.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerNetworkInternalConfig, + Check: resource.ComposeTestCheckFunc( + testAccNetwork("docker_network.foobar", &n), + testAccNetworkInternal(&n, true), + ), + }, + }, + }) +} + +func testAccNetworkInternal(network *dc.Network, internal bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if network.Internal != internal { + return fmt.Errorf("Bad value for attribute 'internal': %t", network.Internal) + } + return nil + } +} + +const testAccDockerNetworkInternalConfig = ` +resource "docker_network" "foobar" { + name = "foobar" + internal = "true" +} +` From 0a75a724b6907b6deb16790efed0fe192dc8742d Mon Sep 17 00:00:00 2001 From: hmcgonig Date: Tue, 3 Jan 2017 11:10:39 -0500 Subject: [PATCH 70/74] provider/docker: Add support for a list of pull_triggers within the docker_image resource. (#10845) --- resource_docker_image.go | 12 +++++++++++- resource_docker_image_funcs.go | 10 +++------- resource_docker_image_test.go | 28 +++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/resource_docker_image.go b/resource_docker_image.go index 9c2f84d4..eb84a257 100644 --- a/resource_docker_image.go +++ b/resource_docker_image.go @@ -28,9 +28,19 @@ func resourceDockerImage() *schema.Resource { }, "pull_trigger": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"pull_triggers"}, + Deprecated: "Use field pull_triggers instead", + }, + + "pull_triggers": &schema.Schema{ + Type: schema.TypeSet, Optional: true, ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, }, } diff --git a/resource_docker_image_funcs.go b/resource_docker_image_funcs.go index 72cbc8ea..9c27b425 100644 --- a/resource_docker_image_funcs.go +++ b/resource_docker_image_funcs.go @@ -188,15 +188,11 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) return nil, fmt.Errorf("Empty image name is not allowed") } - foundImage := searchLocalImages(data, imageName) - - if foundImage == nil { - if err := pullImage(&data, client, imageName); err != nil { - return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err) - } + if err := pullImage(&data, client, imageName); err != nil { + return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err) } - foundImage = searchLocalImages(data, imageName) + foundImage := searchLocalImages(data, imageName) if foundImage != nil { return foundImage, nil } diff --git a/resource_docker_image_test.go b/resource_docker_image_test.go index 484c45e8..4d75a617 100644 --- a/resource_docker_image_test.go +++ b/resource_docker_image_test.go @@ -89,6 +89,22 @@ func TestAccDockerImage_data(t *testing.T) { }) } +func TestAccDockerImage_data_pull_trigger(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerImageFromDataConfigWithPullTrigger, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("docker_image.foobarbazoo", "latest", contentDigestRegexp), + ), + }, + }, + }) +} + func testAccDockerImageDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "docker_image" { @@ -131,6 +147,16 @@ data "docker_registry_image" "foobarbaz" { } resource "docker_image" "foobarbaz" { name = "${data.docker_registry_image.foobarbaz.name}" - pull_trigger = "${data.docker_registry_image.foobarbaz.sha256_digest}" + pull_triggers = ["${data.docker_registry_image.foobarbaz.sha256_digest}"] +} +` + +const testAccDockerImageFromDataConfigWithPullTrigger = ` +data "docker_registry_image" "foobarbazoo" { + name = "alpine:3.1" +} +resource "docker_image" "foobarbazoo" { + name = "${data.docker_registry_image.foobarbazoo.name}" + pull_trigger = "${data.docker_registry_image.foobarbazoo.sha256_digest}" } ` From ae63f9ce265f433d5e1389f2c6ffda6b9f9a5b97 Mon Sep 17 00:00:00 2001 From: Daniel Portella Date: Tue, 7 Mar 2017 16:48:20 +0000 Subject: [PATCH 71/74] provider/docker: added support for linux capabilities (#12045) * added support for linux capabilities Refs #11623 Added capabilities block Added tests for it Added documentation for it. My PC doesnt support memory swap so it errors there. ``` $ make testacc TEST=./builtin/providers/docker TESTARGS='-run=TestAccDockerContainer_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2017/02/17 14:57:08 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/docker -v -run=TestAccDockerContainer_ -timeout 120m === RUN TestAccDockerContainer_basic --- PASS: TestAccDockerContainer_basic (44.50s) === RUN TestAccDockerContainer_volume --- PASS: TestAccDockerContainer_volume (40.73s) === RUN TestAccDockerContainer_customized --- FAIL: TestAccDockerContainer_customized (50.27s) testing.go:265: Step 0 error: Check failed: Check 2/2 error: Container has wrong memory swap setting: -1 Please check that you machine supports memory swap (you can do that by running 'docker info' command). === RUN TestAccDockerContainer_upload --- PASS: TestAccDockerContainer_upload (38.56s) FAIL exit status 1 FAIL github.com/hashicorp/terraform/builtin/providers/docker 174.070s Makefile:48: recipe for target 'testacc' failed make: *** [testacc] Error 1 ``` * Documentation changes. * added maxitems and rerun tests --- resource_docker_container.go | 42 ++++++++++++++++++++++++++++++ resource_docker_container_funcs.go | 9 +++++++ resource_docker_container_test.go | 22 ++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/resource_docker_container.go b/resource_docker_container.go index 2fb4efc2..6bc03de3 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -138,6 +138,33 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "capabilities": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "drop": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + Set: resourceDockerCapabilitiesHash, + }, + "volumes": &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -396,6 +423,21 @@ func resourceDockerContainer() *schema.Resource { } } +func resourceDockerCapabilitiesHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["add"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v)) + } + + if v, ok := m["remove"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v)) + } + + return hashcode.String(buf.String()) +} + func resourceDockerPortsHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index f74264a7..ba7d54aa 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -126,6 +126,15 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hostConfig.VolumesFrom = volumesFrom } + if v, ok := d.GetOk("capabilities"); ok { + for _, capInt := range v.(*schema.Set).List() { + capa := capInt.(map[string]interface{}) + hostConfig.CapAdd = stringSetToStringSlice(capa["add"].(*schema.Set)) + hostConfig.CapDrop = stringSetToStringSlice(capa["drop"].(*schema.Set)) + break + } + } + if v, ok := d.GetOk("dns"); ok { hostConfig.DNS = stringSetToStringSlice(v.(*schema.Set)) } diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 99f0ab3e..1da7e87e 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -128,6 +128,22 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container has wrong dns search setting: %v", c.HostConfig.DNS[0]) } + if len(c.HostConfig.CapAdd) != 1 { + return fmt.Errorf("Container does not have the correct number of Capabilities in ADD: %d", len(c.HostConfig.CapAdd)) + } + + if c.HostConfig.CapAdd[0] != "ALL" { + return fmt.Errorf("Container has wrong CapAdd setting: %v", c.HostConfig.CapAdd[0]) + } + + if len(c.HostConfig.CapDrop) != 1 { + return fmt.Errorf("Container does not have the correct number of Capabilities in Drop: %d", len(c.HostConfig.CapDrop)) + } + + if c.HostConfig.CapDrop[0] != "SYS_ADMIN" { + return fmt.Errorf("Container has wrong CapDrop setting: %v", c.HostConfig.CapDrop[0]) + } + if c.HostConfig.CPUShares != 32 { return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares) } @@ -311,6 +327,12 @@ resource "docker_container" "foo" { memory = 512 memory_swap = 2048 cpu_shares = 32 + + capabilities { + add= ["ALL"] + drop = ["SYS_ADMIN"] + } + dns = ["8.8.8.8"] dns_opts = ["rotate"] dns_search = ["example.com"] From 4e40753b5a664e7cdfb272ad3d4a613a37415eed Mon Sep 17 00:00:00 2001 From: Malte Brodersen Date: Mon, 15 May 2017 12:09:50 +0200 Subject: [PATCH 72/74] Allow Windows Docker containers to map volumes (#13584) * fix regex for windows path * Fix escaping * move validate function out and create test --- resource_docker_container.go | 25 ++++++++++++++----------- resource_docker_container_test.go | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index 6bc03de3..a084ad43 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -184,17 +184,10 @@ func resourceDockerContainer() *schema.Resource { }, "host_path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - value := v.(string) - if !regexp.MustCompile(`^/`).MatchString(value) { - es = append(es, fmt.Errorf( - "%q must be an absolute path", k)) - } - return - }, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateDockerContainerPath, }, "volume_name": &schema.Schema{ @@ -515,3 +508,13 @@ func resourceDockerUploadHash(v interface{}) int { return hashcode.String(buf.String()) } + +func validateDockerContainerPath(v interface{}, k string) (ws []string, errors []error) { + + value := v.(string) + if !regexp.MustCompile(`^[a-zA-Z]:\\|^/`).MatchString(value) { + errors = append(errors, fmt.Errorf("%q must be an absolute path", k)) + } + + return +} diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index 1da7e87e..e9c3bb95 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -27,6 +27,29 @@ func TestAccDockerContainer_basic(t *testing.T) { }) } +func TestAccDockerContainerPath_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + {Value: "/var/log", ErrCount: 0}, + {Value: "/tmp", ErrCount: 0}, + {Value: "C:\\Windows\\System32", ErrCount: 0}, + {Value: "C:\\Program Files\\MSBuild", ErrCount: 0}, + {Value: "test", ErrCount: 1}, + {Value: "C:Test", ErrCount: 1}, + {Value: "", ErrCount: 1}, + } + + for _, tc := range cases { + _, errors := validateDockerContainerPath(tc.Value, "docker_container") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Docker Container Path to trigger a validation error") + } + } +} + func TestAccDockerContainer_volume(t *testing.T) { var c dc.Container From 7446f70146b94eef86513e17f29d8fbe6bcfe3fd Mon Sep 17 00:00:00 2001 From: Januar Date: Mon, 22 May 2017 17:20:32 +0400 Subject: [PATCH 73/74] provider/docker network alias (#14710) * Add Network Alias configuration with network options * Handle case where there's no network option * Handle use case where network option is not available * Handle use case where network option is not available * Network alias only on user defined network * Update documentation for docker provider on network aliases * Remove unused variable * Update documentation * add unit test for docker container network * fix unit test for docker container network --- resource_docker_container.go | 8 ++++++++ resource_docker_container_funcs.go | 9 ++++++++- resource_docker_container_test.go | 11 +++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/resource_docker_container.go b/resource_docker_container.go index a084ad43..543dc930 100644 --- a/resource_docker_container.go +++ b/resource_docker_container.go @@ -376,6 +376,14 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "network_alias": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "network_mode": &schema.Schema{ Type: schema.TypeString, Optional: true, diff --git a/resource_docker_container_funcs.go b/resource_docker_container_funcs.go index ba7d54aa..4a494c5b 100644 --- a/resource_docker_container_funcs.go +++ b/resource_docker_container_funcs.go @@ -188,7 +188,14 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) if v, ok := d.GetOk("networks"); ok { - connectionOpts := dc.NetworkConnectionOptions{Container: retContainer.ID} + var connectionOpts dc.NetworkConnectionOptions + if v, ok := d.GetOk("network_alias"); ok { + endpointConfig := &dc.EndpointConfig{} + endpointConfig.Aliases = stringSetToStringSlice(v.(*schema.Set)) + connectionOpts = dc.NetworkConnectionOptions{Container: retContainer.ID, EndpointConfig: endpointConfig} + } else { + connectionOpts = dc.NetworkConnectionOptions{Container: retContainer.ID} + } for _, rawNetwork := range v.(*schema.Set).List() { network := rawNetwork.(string) diff --git a/resource_docker_container_test.go b/resource_docker_container_test.go index e9c3bb95..dcc6affa 100644 --- a/resource_docker_container_test.go +++ b/resource_docker_container_test.go @@ -203,6 +203,10 @@ func TestAccDockerContainer_customized(t *testing.T) { return fmt.Errorf("Container has incorrect extra host string: %q", c.HostConfig.ExtraHosts[1]) } + if _, ok := c.NetworkSettings.Networks["test"]; !ok { + return fmt.Errorf("Container is not connected to the right user defined network: test") + } + return nil } @@ -370,6 +374,9 @@ resource "docker_container" "foo" { } network_mode = "bridge" + networks = ["${docker_network.test_network.name}"] + network_alias = ["tftest"] + host { host = "testhost" ip = "10.0.1.0" @@ -380,6 +387,10 @@ resource "docker_container" "foo" { ip = "10.0.2.0" } } + +resource "docker_network" "test_network" { + name = "test" +} ` const testAccDockerContainerUploadConfig = ` From e23185d484bbe64c4ea596e301a23d802354eede Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 6 Jun 2017 11:50:32 -0400 Subject: [PATCH 74/74] Transfer docker provider --- config.go => docker/config.go | 0 .../data_source_docker_registry_image.go | 0 .../data_source_docker_registry_image_test.go | 0 provider.go => docker/provider.go | 0 provider_test.go => docker/provider_test.go | 0 .../resource_docker_container.go | 0 .../resource_docker_container_funcs.go | 0 .../resource_docker_container_test.go | 0 resource_docker_image.go => docker/resource_docker_image.go | 0 .../resource_docker_image_funcs.go | 0 .../resource_docker_image_test.go | 0 resource_docker_network.go => docker/resource_docker_network.go | 0 .../resource_docker_network_funcs.go | 0 .../resource_docker_network_test.go | 0 resource_docker_volume.go => docker/resource_docker_volume.go | 0 .../resource_docker_volume_test.go | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename config.go => docker/config.go (100%) rename data_source_docker_registry_image.go => docker/data_source_docker_registry_image.go (100%) rename data_source_docker_registry_image_test.go => docker/data_source_docker_registry_image_test.go (100%) rename provider.go => docker/provider.go (100%) rename provider_test.go => docker/provider_test.go (100%) rename resource_docker_container.go => docker/resource_docker_container.go (100%) rename resource_docker_container_funcs.go => docker/resource_docker_container_funcs.go (100%) rename resource_docker_container_test.go => docker/resource_docker_container_test.go (100%) rename resource_docker_image.go => docker/resource_docker_image.go (100%) rename resource_docker_image_funcs.go => docker/resource_docker_image_funcs.go (100%) rename resource_docker_image_test.go => docker/resource_docker_image_test.go (100%) rename resource_docker_network.go => docker/resource_docker_network.go (100%) rename resource_docker_network_funcs.go => docker/resource_docker_network_funcs.go (100%) rename resource_docker_network_test.go => docker/resource_docker_network_test.go (100%) rename resource_docker_volume.go => docker/resource_docker_volume.go (100%) rename resource_docker_volume_test.go => docker/resource_docker_volume_test.go (100%) diff --git a/config.go b/docker/config.go similarity index 100% rename from config.go rename to docker/config.go diff --git a/data_source_docker_registry_image.go b/docker/data_source_docker_registry_image.go similarity index 100% rename from data_source_docker_registry_image.go rename to docker/data_source_docker_registry_image.go diff --git a/data_source_docker_registry_image_test.go b/docker/data_source_docker_registry_image_test.go similarity index 100% rename from data_source_docker_registry_image_test.go rename to docker/data_source_docker_registry_image_test.go diff --git a/provider.go b/docker/provider.go similarity index 100% rename from provider.go rename to docker/provider.go diff --git a/provider_test.go b/docker/provider_test.go similarity index 100% rename from provider_test.go rename to docker/provider_test.go diff --git a/resource_docker_container.go b/docker/resource_docker_container.go similarity index 100% rename from resource_docker_container.go rename to docker/resource_docker_container.go diff --git a/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go similarity index 100% rename from resource_docker_container_funcs.go rename to docker/resource_docker_container_funcs.go diff --git a/resource_docker_container_test.go b/docker/resource_docker_container_test.go similarity index 100% rename from resource_docker_container_test.go rename to docker/resource_docker_container_test.go diff --git a/resource_docker_image.go b/docker/resource_docker_image.go similarity index 100% rename from resource_docker_image.go rename to docker/resource_docker_image.go diff --git a/resource_docker_image_funcs.go b/docker/resource_docker_image_funcs.go similarity index 100% rename from resource_docker_image_funcs.go rename to docker/resource_docker_image_funcs.go diff --git a/resource_docker_image_test.go b/docker/resource_docker_image_test.go similarity index 100% rename from resource_docker_image_test.go rename to docker/resource_docker_image_test.go diff --git a/resource_docker_network.go b/docker/resource_docker_network.go similarity index 100% rename from resource_docker_network.go rename to docker/resource_docker_network.go diff --git a/resource_docker_network_funcs.go b/docker/resource_docker_network_funcs.go similarity index 100% rename from resource_docker_network_funcs.go rename to docker/resource_docker_network_funcs.go diff --git a/resource_docker_network_test.go b/docker/resource_docker_network_test.go similarity index 100% rename from resource_docker_network_test.go rename to docker/resource_docker_network_test.go diff --git a/resource_docker_volume.go b/docker/resource_docker_volume.go similarity index 100% rename from resource_docker_volume.go rename to docker/resource_docker_volume.go diff --git a/resource_docker_volume_test.go b/docker/resource_docker_volume_test.go similarity index 100% rename from resource_docker_volume_test.go rename to docker/resource_docker_volume_test.go