diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index c853bd23..7806a33e 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -716,6 +716,16 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, Default: false, }, + "source": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_hash": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, }, }, @@ -1473,6 +1483,16 @@ func resourceDockerContainerV1() *schema.Resource { ForceNew: true, Default: false, }, + "source": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_hash": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, }, }, diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index 01fe3c11..3164122a 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "log" "os" "sort" @@ -385,12 +386,23 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err 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") + source := upload.(map[string]interface{})["source"].(string) + + testParams := []string{content, contentBase64, source} + setParams := 0 + for _, v := range testParams { + if v != "" { + setParams++ + } } - if content != "" && contentBase64 != "" { - return fmt.Errorf("Error with upload content: only one of 'content' or 'content_base64' can be specified") + + if setParams == 0 { + return fmt.Errorf("error with upload content: one of 'content', 'content_base64', or 'source' must be set") } + if setParams > 1 { + return fmt.Errorf("error with upload content: only one of 'content', 'content_base64', or 'source' can be set") + } + var contentToUpload string if content != "" { contentToUpload = content @@ -399,6 +411,13 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err decoded, _ := base64.StdEncoding.DecodeString(contentBase64) contentToUpload = string(decoded) } + if source != "" { + sourceContent, err := ioutil.ReadFile(source) + if err != nil { + return fmt.Errorf("could not read file: %s", err) + } + contentToUpload = string(sourceContent) + } file := upload.(map[string]interface{})["file"].(string) executable := upload.(map[string]interface{})["executable"].(bool) diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index e8a70de7..d17c7026 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bytes" "fmt" + "io/ioutil" "os" "reflect" "regexp" @@ -658,6 +659,112 @@ func TestAccDockerContainer_upload(t *testing.T) { }) } +func TestAccDockerContainer_uploadSource(t *testing.T) { + var c types.ContainerJSON + + wd, _ := os.Getwd() + testFile := wd + "/../scripts/testing/testingFile" + testFileContent, _ := ioutil.ReadFile(testFile) + + testCheck := func(*terraform.State) error { + client := testAccProvider.Meta().(*ProviderConfig).DockerClient + + srcPath := "/terraform/test.txt" + 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, "744") { + return fmt.Errorf("File permissions are incorrect: %s", mode) + } + } + + fbuf := new(bytes.Buffer) + fbuf.ReadFrom(tr) + content := fbuf.String() + if content != string(testFileContent) { + return fmt.Errorf("file content is invalid") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDockerContainerUploadSourceConfig, testFile), + 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_uploadSourceHash(t *testing.T) { + var c types.ContainerJSON + var firstRunId string + + wd, _ := os.Getwd() + testFile := wd + "/../scripts/testing/testingFile" + hash, _ := ioutil.ReadFile(testFile + ".base64") + grabFirstCheck := func(*terraform.State) error { + firstRunId = c.ID + return nil + } + testCheck := func(*terraform.State) error { + if c.ID == firstRunId { + return fmt.Errorf("Container should have been recreated due to changed hash") + } + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDockerContainerUploadSourceHashConfig, testFile, string(hash)), + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + grabFirstCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "upload.#", "1"), + ), + }, + { + Config: fmt.Sprintf(testAccDockerContainerUploadSourceHashConfig, testFile, string(hash)+"arbitrary"), + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "upload.#", "1"), + ), + }, + }, + }) +} + func TestAccDockerContainer_uploadAsBase64(t *testing.T) { var c types.ContainerJSON @@ -765,7 +872,7 @@ func TestAccDockerContainer_multipleUploadContentsConfig(t *testing.T) { } } `, - ExpectError: regexp.MustCompile(`.*only one of 'content' or 'content_base64' can be specified.*`), + ExpectError: regexp.MustCompile(`.*only one of 'content', 'content_base64', or 'source' can be set.*`), }, }, }) @@ -794,7 +901,7 @@ func TestAccDockerContainer_noUploadContentsConfig(t *testing.T) { } } `, - ExpectError: regexp.MustCompile(`.* neither 'content', nor 'content_base64' was set.*`), + ExpectError: regexp.MustCompile(`.* one of 'content', 'content_base64', or 'source' must be set.*`), }, }, }) @@ -1791,6 +1898,43 @@ resource "docker_container" "foo" { } ` +const testAccDockerContainerUploadSourceConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + upload { + source = "%s" + file = "/terraform/test.txt" + executable = true + } +} +` + +const testAccDockerContainerUploadSourceHashConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + upload { + source = "%s" + source_hash = "%s" + file = "/terraform/test.txt" + executable = true + } +} +` + const testAccDockerContainerUploadBase64Config = ` resource "docker_image" "foo" { name = "nginx:latest" diff --git a/scripts/testacc_cleanup.sh b/scripts/testacc_cleanup.sh index f8a477bd..d9117808 100755 --- a/scripts/testacc_cleanup.sh +++ b/scripts/testacc_cleanup.sh @@ -4,6 +4,8 @@ set -e for p in $(docker container ls -f 'name=private_registry' -q); do docker stop $p; done echo "### stopped private registry ###" +rm -f "$(pwd)/scripts/testing/testingFile" +rm -f "$(pwd)/scripts/testing/testingFile.base64" rm -f "$(pwd)"/scripts/testing/auth/htpasswd rm -f "$(pwd)"/scripts/testing/certs/registry_auth.* echo "### removed auth and certs ###" diff --git a/scripts/testacc_setup.sh b/scripts/testacc_setup.sh index 34766bab..6441c748 100755 --- a/scripts/testacc_setup.sh +++ b/scripts/testacc_setup.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +echo -n "foo" > "$(pwd)/scripts/testing/testingFile" +echo -n `base64 $(pwd)/scripts/testing/testingFile` > "$(pwd)/scripts/testing/testingFile.base64" + # Create self signed certs mkdir -p "$(pwd)"/scripts/testing/certs openssl req \ diff --git a/website/docs/r/container.html.markdown b/website/docs/r/container.html.markdown index 9c214e8e..9fb57c92 100644 --- a/website/docs/r/container.html.markdown +++ b/website/docs/r/container.html.markdown @@ -220,8 +220,10 @@ files to upload to the container before starting it. Only one of `content` or `c one of them hast to be set. Each `upload` supports the following -* `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. +* `content` - (Optional, string, conflicts with `content_base64` & `source`) 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` & `source`) 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. +* `source` - (Optional, string, conflicts with `content` & `content_base64`) A filename that references a file which will be uploaded as the object content. This allows for large file uploads that do not get stored in state. +* `source_hash` - (Optional, string) If using `source`, this will force an update if the file content has updated but the filename has not. * `file` - (Required, string) path to a file in the container. * `executable` - (Optional, bool) If true, the file will be uploaded with user executable permission.