fix: binary upload as base 64 content (#194)

Closes #48 
* feat: adds new content_base64 property for upload
* feat: adds logic for base64 content processing
* test: for new upload property
* docs: for new upload property
* docs: adapts container upload to aws example docs
* fix: adds a comment for non-working conflicts with within lists
* docs: updates for non-supported conflict within lists and sets
* tests: updates notes on disabled checks
* fix: validation for container upload configs
* docs: validation for container upload configs
* fix(test): add must not run flag for invalid upload config container
This commit is contained in:
Manuel Vogel 2019-10-07 23:00:57 +02:00 committed by GitHub
parent cb9c327ae4
commit 2ec6bba9b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 206 additions and 5 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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"

View file

@ -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.