diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 1e61e7a9..c2313c1f 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -659,11 +659,17 @@ func resourceDockerContainer() *schema.Resource { Schema: map[string]*schema.Schema{ "content": { Type: schema.TypeString, - Required: true, + Optional: 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, }, + "content_base64": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateStringIsBase64Encoded(), + }, "file": { Type: schema.TypeString, Required: true, diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index 6c0a6ea4..40523f35 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bufio" "bytes" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -382,6 +383,21 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err var mode int64 for _, upload := range v.(*schema.Set).List() { content := upload.(map[string]interface{})["content"].(string) + contentBase64 := upload.(map[string]interface{})["content_base64"].(string) + if content == "" && contentBase64 == "" { + return fmt.Errorf("Error with upload content: neither 'content', nor 'content_base64' was set") + } + if content != "" && contentBase64 != "" { + return fmt.Errorf("Error with upload content: only one of 'content' or 'content_base64' can be specified") + } + var contentToUpload string + if content != "" { + contentToUpload = content + } + if contentBase64 != "" { + decoded, _ := base64.StdEncoding.DecodeString(contentBase64) + contentToUpload = string(decoded) + } file := upload.(map[string]interface{})["file"].(string) executable := upload.(map[string]interface{})["executable"].(bool) @@ -395,12 +411,12 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err hdr := &tar.Header{ Name: file, Mode: mode, - Size: int64(len(content)), + Size: int64(len(contentToUpload)), } if err := tw.WriteHeader(hdr); err != nil { return fmt.Errorf("Error creating tar archive: %s", err) } - if _, err := tw.Write([]byte(content)); err != nil { + if _, err := tw.Write([]byte(contentToUpload)); err != nil { return fmt.Errorf("Error creating tar archive: %s", err) } if err := tw.Close(); err != nil { diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index decdd528..d337e3aa 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "reflect" + "regexp" "strconv" "strings" "testing" @@ -610,12 +611,164 @@ func TestAccDockerContainer_upload(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccContainerRunning("docker_container.foo", &c), testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "upload.#", "1"), + // NOTE mavogel: current the terraform-plugin-sdk it's likely that + // the acceptance testing framework shims (still using the older flatmap-style addressing) + // are missing a conversion with the hashes. + // See https://github.com/hashicorp/terraform-plugin-sdk/issues/196 + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content", "foo"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content_base64", ""), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.executable", "true"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.file", "/terraform/test.txt"), ), }, }, }) } +func TestAccDockerContainer_uploadAsBase64(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(srcPath, wantedContent, filePerm string) func(*terraform.State) error { + return func(*terraform.State) error { + client := testAccProvider.Meta().(*ProviderConfig).DockerClient + + r, _, err := client.CopyFromContainer(context.Background(), c.ID, srcPath) + if err != nil { + return fmt.Errorf("Unable to download a file from container: %s", err) + } + + tr := tar.NewReader(r) + if header, err := tr.Next(); err != nil { + return fmt.Errorf("Unable to read content of tar archive: %s", err) + } else { + mode := strconv.FormatInt(header.Mode, 8) + if !strings.HasSuffix(mode, filePerm) { + return fmt.Errorf("File permissions are incorrect: %s", mode) + } + } + + fbuf := new(bytes.Buffer) + fbuf.ReadFrom(tr) + gotContent := fbuf.String() + + if wantedContent != gotContent { + return fmt.Errorf("file content is invalid: want: %q, got: %q", wantedContent, gotContent) + } + + return nil + } + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDockerContainerUploadBase64Config, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck("/terraform/test1.txt", "894fc3f56edf2d3a4c5fb5cb71df910f958a2ed8", "744"), + testCheck("/terraform/test2.txt", "foobar", "100644"), + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "upload.#", "2"), + // NOTE: see comment above + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content", ""), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content_base64", "ODk0ZmMzZjU2ZWRmMmQzYTRjNWZiNWNiNzFkZjkxMGY5NThhMmVkOA=="), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.executable", "true"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.file", "/terraform/test1.txt"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.content", "foo"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.content_base64", ""), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.executable", "false"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.file", "/terraform/test2.txt"), + ), + }, + // We add a second on purpose to detect if there is a dirty plan + // although the file content did not change + { + Config: testAccDockerContainerUploadBase64Config, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck("/terraform/test1.txt", "894fc3f56edf2d3a4c5fb5cb71df910f958a2ed8", "744"), + testCheck("/terraform/test2.txt", "foobar", "100644"), + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "upload.#", "2"), + // NOTE: see comment above + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content", ""), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.content_base64", "ODk0ZmMzZjU2ZWRmMmQzYTRjNWZiNWNiNzFkZjkxMGY5NThhMmVkOA=="), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.executable", "true"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.0.file", "/terraform/test1.txt"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.content", "foo"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.content_base64", ""), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.executable", "false"), + // resource.TestCheckResourceAttr("docker_container.foo", "upload.1.file", "/terraform/test2.txt"), + ), + }, + }, + }) +} + +func TestAccDockerContainer_multipleUploadContentsConfig(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ` + resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true + } + + resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + must_run = "false" + + upload { + content = "foobar" + content_base64 = "${base64encode("barbaz")}" + file = "/terraform/test1.txt" + executable = true + } + } + `, + ExpectError: regexp.MustCompile(`.*only one of 'content' or 'content_base64' can be specified.*`), + }, + }, + }) +} + +func TestAccDockerContainer_noUploadContentsConfig(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ` + resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true + } + + resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + must_run = "false" + + upload { + file = "/terraform/test1.txt" + executable = true + } + } + `, + ExpectError: regexp.MustCompile(`.* neither 'content', nor 'content_base64' was set.*`), + }, + }, + }) +} + func TestAccDockerContainer_device(t *testing.T) { var c types.ContainerJSON @@ -1531,6 +1684,7 @@ resource "docker_network" "test_network" { const testAccDockerContainerUploadConfig = ` resource "docker_image" "foo" { name = "nginx:latest" + keep_locally = true } resource "docker_container" "foo" { @@ -1545,6 +1699,29 @@ resource "docker_container" "foo" { } ` +const testAccDockerContainerUploadBase64Config = ` +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + upload { + content_base64 = "${base64encode("894fc3f56edf2d3a4c5fb5cb71df910f958a2ed8")}" + file = "/terraform/test1.txt" + executable = true + } + + upload { + content = "foobar" + file = "/terraform/test2.txt" + } +} +` + const testAccDockerContainerDeviceConfig = ` resource "docker_image" "foo" { name = "nginx:latest" diff --git a/website/docs/r/container.html.markdown b/website/docs/r/container.html.markdown index 97e2cee2..89f1b187 100644 --- a/website/docs/r/container.html.markdown +++ b/website/docs/r/container.html.markdown @@ -204,10 +204,12 @@ One of `from_container`, `host_path` or `volume_name` must be set. ### File Upload `upload` is a block within the configuration that can be repeated to specify -files to upload to the container before starting it. +files to upload to the container before starting it. Only one of `content` or `content_base64` can be set and at least +one of them hast to be set. Each `upload` supports the following -* `content` - (Required, string) A content of a file to upload. +* `content` - (Optional, string, conflicts with `content_base64`) Literal string value to use as the object content, which will be uploaded as UTF-8-encoded text. +* `content_base64` - (Optional, string, conflicts with `content`) Base64-encoded data that will be decoded and uploaded as raw bytes for the object content. This allows safely uploading non-UTF8 binary data, but is recommended only for larger binary content such as the result of the `base64encode` interpolation function. See [here](https://github.com/terraform-providers/terraform-provider-docker/issues/48#issuecomment-374174588) for the reason. * `file` - (Required, string) path to a file in the container. * `executable` - (Optional, bool) If true, the file will be uploaded with user executable permission.