mirror of
https://github.com/kreuzwerker/terraform-provider-docker.git
synced 2026-02-15 16:48:37 -05:00
Merge branch 'master' of /Users/jake/terraform
This commit is contained in:
commit
ed37678895
16 changed files with 2730 additions and 0 deletions
49
docker/config.go
Normal file
49
docker/config.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
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
|
||||
Ca string
|
||||
Cert string
|
||||
Key string
|
||||
CertPath string
|
||||
}
|
||||
|
||||
// NewClient() returns a new Docker client.
|
||||
func (c *Config) NewClient() (*dc.Client, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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.
|
||||
type Data struct {
|
||||
DockerImages map[string]*dc.APIImages
|
||||
}
|
||||
166
docker/data_source_docker_registry_image.go
Normal file
166
docker/data_source_docker_registry_image.go
Normal file
|
|
@ -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
|
||||
}
|
||||
52
docker/data_source_docker_registry_image_test.go
Normal file
52
docker/data_source_docker_registry_image_test.go
Normal file
|
|
@ -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"
|
||||
}
|
||||
`
|
||||
82
docker/provider.go
Normal file
82
docker/provider.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func Provider() terraform.ResourceProvider {
|
||||
return &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"host": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
||||
Description: "The Docker daemon address",
|
||||
},
|
||||
|
||||
"ca_material": &schema.Schema{
|
||||
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", ""),
|
||||
Description: "PEM-encoded content of Docker client certificate",
|
||||
},
|
||||
"key_material": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""),
|
||||
Description: "PEM-encoded content of Docker client private key",
|
||||
},
|
||||
|
||||
"cert_path": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", ""),
|
||||
Description: "Path to directory with Docker TLS config",
|
||||
},
|
||||
},
|
||||
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"docker_container": resourceDockerContainer(),
|
||||
"docker_image": resourceDockerImage(),
|
||||
"docker_network": resourceDockerNetwork(),
|
||||
"docker_volume": resourceDockerVolume(),
|
||||
},
|
||||
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"docker_registry_image": dataSourceDockerRegistryImage(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
36
docker/provider_test.go
Normal file
36
docker/provider_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
528
docker/resource_docker_container.go
Normal file
528
docker/resource_docker_container.go
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"regexp"
|
||||
|
||||
"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},
|
||||
},
|
||||
|
||||
"entrypoint": &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
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,
|
||||
ForceNew: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
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,
|
||||
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|unless-stopped)$`).MatchString(value) {
|
||||
es = append(es, fmt.Errorf(
|
||||
"%q must be one of \"no\", \"on-failure\", \"always\" or \"unless-stopped\"", k))
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
|
||||
"max_retry_count": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
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,
|
||||
ForceNew: true,
|
||||
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,
|
||||
ValidateFunc: validateDockerContainerPath,
|
||||
},
|
||||
|
||||
"volume_name": &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: &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,
|
||||
},
|
||||
|
||||
"host": &schema.Schema{
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"ip": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"host": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Set: resourceDockerHostsHash,
|
||||
},
|
||||
|
||||
"env": &schema.Schema{
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
Set: schema.HashString,
|
||||
},
|
||||
|
||||
"links": &schema.Schema{
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
Set: schema.HashString,
|
||||
},
|
||||
|
||||
"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,
|
||||
},
|
||||
|
||||
"privileged": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"destroy_grace_seconds": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"labels": &schema.Schema{
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"memory": &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
|
||||
},
|
||||
},
|
||||
|
||||
"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{
|
||||
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,
|
||||
},
|
||||
|
||||
"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,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"networks": &schema.Schema{
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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{})
|
||||
|
||||
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 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{})
|
||||
|
||||
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["volume_name"]; 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 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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
465
docker/resource_docker_container_funcs.go
Normal file
465
docker/resource_docker_container_funcs.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dc "github.com/fsouza/go-dockerclient"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
creationTime time.Time
|
||||
)
|
||||
|
||||
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
var err error
|
||||
client := meta.(*dc.Client)
|
||||
|
||||
var data Data
|
||||
if err := fetchLocalImages(&data, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image := d.Get("image").(string)
|
||||
if _, ok := data.DockerImages[image]; !ok {
|
||||
if _, ok := data.DockerImages[image+":latest"]; !ok {
|
||||
return fmt.Errorf("Unable to find image %s", image)
|
||||
}
|
||||
image = image + ":latest"
|
||||
}
|
||||
|
||||
// The awesome, wonderful, splendiferous, sensical
|
||||
// Docker API now lets you specify a HostConfig in
|
||||
// CreateContainerOptions, but in my testing it still only
|
||||
// actually applies HostConfig options set in StartContainer.
|
||||
// How cool is that?
|
||||
createOpts := dc.CreateContainerOptions{
|
||||
Name: d.Get("name").(string),
|
||||
Config: &dc.Config{
|
||||
Image: image,
|
||||
Hostname: d.Get("hostname").(string),
|
||||
Domainname: d.Get("domainname").(string),
|
||||
},
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("env"); ok {
|
||||
createOpts.Config.Env = stringSetToStringSlice(v.(*schema.Set))
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("command"); ok {
|
||||
createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{}))
|
||||
for _, v := range createOpts.Config.Cmd {
|
||||
if v == "" {
|
||||
return fmt.Errorf("values for command may not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("entrypoint"); ok {
|
||||
createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{}))
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("user"); ok {
|
||||
createOpts.Config.User = v.(string)
|
||||
}
|
||||
|
||||
exposedPorts := map[dc.Port]struct{}{}
|
||||
portBindings := map[dc.Port][]dc.PortBinding{}
|
||||
|
||||
if v, ok := d.GetOk("ports"); ok {
|
||||
exposedPorts, portBindings = portSetToDockerPorts(v.(*schema.Set))
|
||||
}
|
||||
if len(exposedPorts) != 0 {
|
||||
createOpts.Config.ExposedPorts = exposedPorts
|
||||
}
|
||||
|
||||
extraHosts := []string{}
|
||||
if v, ok := d.GetOk("host"); ok {
|
||||
extraHosts = extraHostsSetToDockerExtraHosts(v.(*schema.Set))
|
||||
}
|
||||
|
||||
volumes := map[string]struct{}{}
|
||||
binds := []string{}
|
||||
volumesFrom := []string{}
|
||||
|
||||
if v, ok := d.GetOk("volumes"); ok {
|
||||
volumes, binds, volumesFrom, err = volumeSetToDockerVolumes(v.(*schema.Set))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse volumes: %s", err)
|
||||
}
|
||||
}
|
||||
if len(volumes) != 0 {
|
||||
createOpts.Config.Volumes = volumes
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("labels"); ok {
|
||||
createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{}))
|
||||
}
|
||||
|
||||
hostConfig := &dc.HostConfig{
|
||||
Privileged: d.Get("privileged").(bool),
|
||||
PublishAllPorts: d.Get("publish_all_ports").(bool),
|
||||
RestartPolicy: dc.RestartPolicy{
|
||||
Name: d.Get("restart").(string),
|
||||
MaximumRetryCount: d.Get("max_retry_count").(int),
|
||||
},
|
||||
LogConfig: dc.LogConfig{
|
||||
Type: d.Get("log_driver").(string),
|
||||
},
|
||||
}
|
||||
|
||||
if len(portBindings) != 0 {
|
||||
hostConfig.PortBindings = portBindings
|
||||
}
|
||||
if len(extraHosts) != 0 {
|
||||
hostConfig.ExtraHosts = extraHosts
|
||||
}
|
||||
if len(binds) != 0 {
|
||||
hostConfig.Binds = binds
|
||||
}
|
||||
if len(volumesFrom) != 0 {
|
||||
hostConfig.VolumesFrom = volumesFrom
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("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))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("memory"); ok {
|
||||
hostConfig.Memory = int64(v.(int)) * 1024 * 1024
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("memory_swap"); ok {
|
||||
swap := int64(v.(int))
|
||||
if swap > 0 {
|
||||
swap = swap * 1024 * 1024
|
||||
}
|
||||
hostConfig.MemorySwap = swap
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("cpu_shares"); ok {
|
||||
hostConfig.CPUShares = int64(v.(int))
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("log_opts"); ok {
|
||||
hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{}))
|
||||
}
|
||||
|
||||
if v, ok := d.GetOk("network_mode"); ok {
|
||||
hostConfig.NetworkMode = v.(string)
|
||||
}
|
||||
|
||||
createOpts.HostConfig = hostConfig
|
||||
|
||||
var retContainer *dc.Container
|
||||
if retContainer, err = client.CreateContainer(createOpts); err != nil {
|
||||
return fmt.Errorf("Unable to create container: %s", err)
|
||||
}
|
||||
if retContainer == nil {
|
||||
return fmt.Errorf("Returned container is nil")
|
||||
}
|
||||
|
||||
d.SetId(retContainer.ID)
|
||||
|
||||
if v, ok := d.GetOk("networks"); ok {
|
||||
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)
|
||||
if err := client.ConnectNetwork(network, connectionOpts); err != nil {
|
||||
return fmt.Errorf("Unable to connect to network '%s': %s", network, 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)
|
||||
}
|
||||
|
||||
return resourceDockerContainerRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
|
||||
apiContainer, err := fetchDockerContainer(d.Id(), client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apiContainer == nil {
|
||||
// This container doesn't exist anymore
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
var container *dc.Container
|
||||
|
||||
loops := 1 // if it hasn't just been created, don't delay
|
||||
if !creationTime.IsZero() {
|
||||
loops = 30 // with 500ms spacing, 15 seconds; ought to be plenty
|
||||
}
|
||||
sleepTime := 500 * time.Millisecond
|
||||
|
||||
for i := loops; i > 0; i-- {
|
||||
container, err = client.InspectContainer(apiContainer.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error inspecting container %s: %s", apiContainer.ID, err)
|
||||
}
|
||||
|
||||
if container.State.Running ||
|
||||
!container.State.Running && !d.Get("must_run").(bool) {
|
||||
break
|
||||
}
|
||||
|
||||
if creationTime.IsZero() { // We didn't just create it, so don't wait around
|
||||
return resourceDockerContainerDelete(d, meta)
|
||||
}
|
||||
|
||||
if container.State.FinishedAt.After(creationTime) {
|
||||
// It exited immediately, so error out so dependent containers
|
||||
// aren't started
|
||||
resourceDockerContainerDelete(d, meta)
|
||||
return fmt.Errorf("Container %s exited after creation, error was: %s", apiContainer.ID, container.State.Error)
|
||||
}
|
||||
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
|
||||
// Handle the case of the for loop above running its course
|
||||
if !container.State.Running && d.Get("must_run").(bool) {
|
||||
resourceDockerContainerDelete(d, meta)
|
||||
return fmt.Errorf("Container %s failed to be in running state", apiContainer.ID)
|
||||
}
|
||||
|
||||
// Read Network Settings
|
||||
if container.NetworkSettings != nil {
|
||||
d.Set("ip_address", container.NetworkSettings.IPAddress)
|
||||
d.Set("ip_prefix_length", container.NetworkSettings.IPPrefixLen)
|
||||
d.Set("gateway", container.NetworkSettings.Gateway)
|
||||
d.Set("bridge", container.NetworkSettings.Bridge)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
|
||||
// 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,
|
||||
Force: true,
|
||||
}
|
||||
|
||||
if err := client.RemoveContainer(removeOpts); err != nil {
|
||||
return fmt.Errorf("Error deleting container %s: %s", d.Id(), err)
|
||||
}
|
||||
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringListToStringSlice(stringList []interface{}) []string {
|
||||
ret := []string{}
|
||||
for _, v := range stringList {
|
||||
if v == nil {
|
||||
ret = append(ret, "")
|
||||
continue
|
||||
}
|
||||
ret = append(ret, v.(string))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func stringSetToStringSlice(stringSet *schema.Set) []string {
|
||||
ret := []string{}
|
||||
if stringSet == nil {
|
||||
return ret
|
||||
}
|
||||
for _, envVal := range stringSet.List() {
|
||||
ret = append(ret, envVal.(string))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func mapTypeMapValsToString(typeMap map[string]interface{}) map[string]string {
|
||||
mapped := make(map[string]string, len(typeMap))
|
||||
for k, v := range typeMap {
|
||||
mapped[k] = v.(string)
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
|
||||
func fetchDockerContainer(ID string, client *dc.Client) (*dc.APIContainers, error) {
|
||||
apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching container information from Docker: %s\n", err)
|
||||
}
|
||||
|
||||
for _, apiContainer := range apiContainers {
|
||||
if apiContainer.ID == ID {
|
||||
return &apiContainer, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func portSetToDockerPorts(ports *schema.Set) (map[dc.Port]struct{}, map[dc.Port][]dc.PortBinding) {
|
||||
retExposedPorts := map[dc.Port]struct{}{}
|
||||
retPortBindings := map[dc.Port][]dc.PortBinding{}
|
||||
|
||||
for _, portInt := range ports.List() {
|
||||
port := portInt.(map[string]interface{})
|
||||
internal := port["internal"].(int)
|
||||
protocol := port["protocol"].(string)
|
||||
|
||||
exposedPort := dc.Port(strconv.Itoa(internal) + "/" + protocol)
|
||||
retExposedPorts[exposedPort] = struct{}{}
|
||||
|
||||
external, extOk := port["external"].(int)
|
||||
ip, ipOk := port["ip"].(string)
|
||||
|
||||
if extOk {
|
||||
portBinding := dc.PortBinding{
|
||||
HostPort: strconv.Itoa(external),
|
||||
}
|
||||
if ipOk {
|
||||
portBinding.HostIP = ip
|
||||
}
|
||||
retPortBindings[exposedPort] = append(retPortBindings[exposedPort], portBinding)
|
||||
}
|
||||
}
|
||||
|
||||
return retExposedPorts, retPortBindings
|
||||
}
|
||||
|
||||
func extraHostsSetToDockerExtraHosts(extraHosts *schema.Set) []string {
|
||||
retExtraHosts := []string{}
|
||||
|
||||
for _, hostInt := range extraHosts.List() {
|
||||
host := hostInt.(map[string]interface{})
|
||||
ip := host["ip"].(string)
|
||||
hostname := host["host"].(string)
|
||||
retExtraHosts = append(retExtraHosts, hostname+":"+ip)
|
||||
}
|
||||
|
||||
return retExtraHosts
|
||||
}
|
||||
|
||||
func volumeSetToDockerVolumes(volumes *schema.Set) (map[string]struct{}, []string, []string, error) {
|
||||
retVolumeMap := map[string]struct{}{}
|
||||
retHostConfigBinds := []string{}
|
||||
retVolumeFromContainers := []string{}
|
||||
|
||||
for _, volumeInt := range volumes.List() {
|
||||
volume := volumeInt.(map[string]interface{})
|
||||
fromContainer := volume["from_container"].(string)
|
||||
containerPath := volume["container_path"].(string)
|
||||
volumeName := volume["volume_name"].(string)
|
||||
if len(volumeName) == 0 {
|
||||
volumeName = volume["host_path"].(string)
|
||||
}
|
||||
readOnly := volume["read_only"].(bool)
|
||||
|
||||
switch {
|
||||
case len(fromContainer) == 0 && len(containerPath) == 0:
|
||||
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Volume entry without container path or source container")
|
||||
case len(fromContainer) != 0 && len(containerPath) != 0:
|
||||
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, errors.New("Both a container and a path specified in a volume entry")
|
||||
case len(fromContainer) != 0:
|
||||
retVolumeFromContainers = append(retVolumeFromContainers, fromContainer)
|
||||
case len(volumeName) != 0:
|
||||
readWrite := "rw"
|
||||
if readOnly {
|
||||
readWrite = "ro"
|
||||
}
|
||||
retVolumeMap[containerPath] = struct{}{}
|
||||
retHostConfigBinds = append(retHostConfigBinds, volumeName+":"+containerPath+":"+readWrite)
|
||||
default:
|
||||
retVolumeMap[containerPath] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return retVolumeMap, retHostConfigBinds, retVolumeFromContainers, nil
|
||||
}
|
||||
410
docker/resource_docker_container_test.go
Normal file
410
docker/resource_docker_container_test.go
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"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) {
|
||||
var c dc.Container
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerContainerConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccContainerRunning("docker_container.foo", &c),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
testCheck := func(*terraform.State) error {
|
||||
if len(c.Mounts) != 1 {
|
||||
return fmt.Errorf("Incorrect number of mounts: expected 1, 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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Container has wrong restart policy: %s", c.HostConfig.RestartPolicy.Name)
|
||||
}
|
||||
|
||||
if c.HostConfig.Memory != (512 * 1024 * 1024) {
|
||||
return fmt.Errorf("Container has wrong memory setting: %d", c.HostConfig.Memory)
|
||||
}
|
||||
|
||||
if c.HostConfig.MemorySwap != (2048 * 1024 * 1024) {
|
||||
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 {
|
||||
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 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)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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"])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
if _, ok := c.NetworkSettings.Networks["test"]; !ok {
|
||||
return fmt.Errorf("Container is not connected to the right user defined network: test")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerContainerCustomizedConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccContainerRunning("docker_container.foo", &c),
|
||||
testCheck,
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
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 {
|
||||
inspected, err := client.InspectContainer(c.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Container could not be inspected: %s", err)
|
||||
}
|
||||
*container = *inspected
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Container not found: %s", rs.Primary.ID)
|
||||
}
|
||||
}
|
||||
|
||||
const testAccDockerContainerConfig = `
|
||||
resource "docker_image" "foo" {
|
||||
name = "nginx:latest"
|
||||
}
|
||||
|
||||
resource "docker_container" "foo" {
|
||||
name = "tf-test"
|
||||
image = "${docker_image.foo.latest}"
|
||||
}
|
||||
`
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
resource "docker_container" "foo" {
|
||||
name = "tf-test"
|
||||
image = "${docker_image.foo.latest}"
|
||||
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
|
||||
cpu_shares = 32
|
||||
|
||||
capabilities {
|
||||
add= ["ALL"]
|
||||
drop = ["SYS_ADMIN"]
|
||||
}
|
||||
|
||||
dns = ["8.8.8.8"]
|
||||
dns_opts = ["rotate"]
|
||||
dns_search = ["example.com"]
|
||||
labels {
|
||||
env = "prod"
|
||||
role = "test"
|
||||
}
|
||||
log_driver = "json-file"
|
||||
log_opts = {
|
||||
max-size = "10m"
|
||||
max-file = 20
|
||||
}
|
||||
network_mode = "bridge"
|
||||
|
||||
networks = ["${docker_network.test_network.name}"]
|
||||
network_alias = ["tftest"]
|
||||
|
||||
host {
|
||||
host = "testhost"
|
||||
ip = "10.0.1.0"
|
||||
}
|
||||
|
||||
host {
|
||||
host = "testhost2"
|
||||
ip = "10.0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_network" "test_network" {
|
||||
name = "test"
|
||||
}
|
||||
`
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
`
|
||||
47
docker/resource_docker_image.go
Normal file
47
docker/resource_docker_image.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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,
|
||||
},
|
||||
|
||||
"latest": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
|
||||
"keep_locally": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"pull_trigger": &schema.Schema{
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
201
docker/resource_docker_image_funcs.go
Normal file
201
docker/resource_docker_image_funcs.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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 {
|
||||
client := meta.(*dc.Client)
|
||||
apiImage, err := findImage(d, client)
|
||||
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 {
|
||||
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 {
|
||||
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
|
||||
}
|
||||
|
||||
d.Set("latest", apiImage.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 keepLocally := d.Get("keep_locally").(bool); keepLocally {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
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.
|
||||
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 := 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, ":")
|
||||
switch len(splitImageName) {
|
||||
|
||||
// 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 = pullOpts.Registry + "/" + strings.Join(splitPortRepo[1:], "/")
|
||||
|
||||
// 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]
|
||||
|
||||
// registry:port/username/repo or registry:port/repo
|
||||
default:
|
||||
pullOpts.Registry = splitImageName[0] + ":" + splitPortRepo[0]
|
||||
pullOpts.Repository = pullOpts.Registry + "/" + strings.Join(splitPortRepo[1:], "/")
|
||||
pullOpts.Tag = "latest"
|
||||
}
|
||||
|
||||
// Plain username/repo or repo
|
||||
default:
|
||||
pullOpts.Repository = image
|
||||
}
|
||||
|
||||
return pullOpts
|
||||
}
|
||||
|
||||
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
|
||||
var data Data
|
||||
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")
|
||||
}
|
||||
|
||||
if err := pullImage(&data, client, imageName); err != nil {
|
||||
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
||||
}
|
||||
|
||||
foundImage := searchLocalImages(data, imageName)
|
||||
if foundImage != nil {
|
||||
return foundImage, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unable to find or pull image %s", imageName)
|
||||
}
|
||||
162
docker/resource_docker_image_test.go
Normal file
162
docker/resource_docker_image_test.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
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,
|
||||
CheckDestroy: testAccDockerImageDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerImageConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("docker_image.foo", "latest", contentDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccDockerImage_private(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccDockerImageDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAddDockerPrivateImageConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("docker_image.foobar", "latest", contentDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 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 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" {
|
||||
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"
|
||||
}
|
||||
`
|
||||
|
||||
const testAddDockerPrivateImageConfig = `
|
||||
resource "docker_image" "foobar" {
|
||||
name = "gcr.io:443/google_containers/pause:0.8.0"
|
||||
}
|
||||
`
|
||||
|
||||
const testAccDockerImageKeepLocallyConfig = `
|
||||
resource "docker_image" "foobarzoo" {
|
||||
name = "crux:3.1"
|
||||
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_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}"
|
||||
}
|
||||
`
|
||||
142
docker/resource_docker_network.go
Normal file
142
docker/resource_docker_network.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
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,
|
||||
},
|
||||
|
||||
"internal": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: 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())
|
||||
}
|
||||
122
docker/resource_docker_network_funcs.go
Normal file
122
docker/resource_docker_network_funcs.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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{})
|
||||
}
|
||||
if v, ok := d.GetOk("internal"); ok {
|
||||
createOpts.Internal = v.(bool)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// The 'internal' property is not send back when create network
|
||||
d.Set("internal", createOpts.Internal)
|
||||
|
||||
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)
|
||||
d.Set("internal", retNetwork.Internal)
|
||||
|
||||
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
|
||||
}
|
||||
99
docker/resource_docker_network_test.go
Normal file
99
docker/resource_docker_network_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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"
|
||||
}
|
||||
`
|
||||
|
||||
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"
|
||||
}
|
||||
`
|
||||
102
docker/resource_docker_volume.go
Normal file
102
docker/resource_docker_volume.go
Normal file
|
|
@ -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
|
||||
}
|
||||
67
docker/resource_docker_volume_test.go
Normal file
67
docker/resource_docker_volume_test.go
Normal file
|
|
@ -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"
|
||||
}
|
||||
`
|
||||
Loading…
Reference in a new issue