Skip to content

Commit 68cc59d

Browse files
authored
fix(internal/provider): correct escaping of strings in envbuilder_cached_image.env (#32)
Fixes #31 We had previously been doing the equivalent of value.String() when writing envbuilder_cached_image.env. This was incorrectly escaping newlines, potentially breaking ENVBUILDER_INIT_SCRIPT. This PR modifies the behaviour to correctly handle string values via ValueString() instead.
1 parent b35004a commit 68cc59d

File tree

3 files changed

+36
-25
lines changed

3 files changed

+36
-25
lines changed

Diff for: examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf

+4-4
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,20 @@ resource "envbuilder_cached_image" "example" {
6666
builder_image = var.builder_image
6767
git_url = var.repo_url
6868
cache_repo = local.cache_repo
69+
insecure = true
6970
extra_env = {
7071
"ENVBUILDER_VERBOSE" : "true"
7172
"ENVBUILDER_INSECURE" : "true" # due to local registry
72-
"ENVBUILDER_INIT_SCRIPT" : "sleep infinity"
73+
"ENVBUILDER_INIT_SCRIPT" : "#!/usr/bin/env bash\necho Hello && sleep infinity"
7374
"ENVBUILDER_PUSH_IMAGE" : "true"
7475
}
7576
depends_on = [docker_container.registry]
7677
}
7778

7879
// Run the cached image. Depending on the contents of
7980
// the cache repo, this will either be var.builder_image
80-
// or a previously built image pusehd to var.cache_repo.
81-
// Running `terraform apply` once (assuming empty cache)
81+
// or a previously built image pushed to var.cache_repo.
82+
// Running `terraform apply` once (assuming empty cache)
8283
// will result in the builder image running, and the built
8384
// image being pushed to the cache repo.
8485
// Running `terraform apply` again will result in the
@@ -105,4 +106,3 @@ output "id" {
105106
output "image" {
106107
value = envbuilder_cached_image.example.image
107108
}
108-

Diff for: internal/provider/cached_image_resource.go

+26-16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/google/go-containerregistry/pkg/v1/remote"
2323
"github.com/google/uuid"
2424

25+
"github.com/hashicorp/terraform-plugin-framework/attr"
2526
"github.com/hashicorp/terraform-plugin-framework/resource"
2627
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2728
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
@@ -631,34 +632,43 @@ func extractEnvbuilderFromImage(ctx context.Context, imgRef, destPath string) er
631632
return fmt.Errorf("extract envbuilder binary from image %q: %w", imgRef, os.ErrNotExist)
632633
}
633634

634-
// NOTE: the String() method of Terraform values will evalue to `<null>` if unknown.
635-
// Check IsUnknown() first before calling String().
636-
type stringable interface {
637-
IsUnknown() bool
638-
IsNull() bool
639-
String() string
635+
// tfValueToString converts an attr.Value to its string representation
636+
// based on its Terraform type. This is needed because the String()
637+
// method on an attr.Value creates a 'human-readable' version of the type, which
638+
// leads to quotes, escaped characters, and other assorted sadness.
639+
func tfValueToString(val attr.Value) string {
640+
if val.IsUnknown() || val.IsNull() {
641+
return ""
642+
}
643+
if vs, ok := val.(interface{ ValueString() string }); ok {
644+
return vs.ValueString()
645+
}
646+
if vb, ok := val.(interface{ ValueBool() bool }); ok {
647+
return fmt.Sprintf("%t", vb.ValueBool())
648+
}
649+
if vi, ok := val.(interface{ ValueInt64() int64 }); ok {
650+
return fmt.Sprintf("%d", vi.ValueInt64())
651+
}
652+
panic(fmt.Errorf("tfValueToString: value %T is not a supported type", val))
640653
}
641654

642-
func appendKnownEnvToList(list types.List, key string, value stringable) types.List {
655+
func appendKnownEnvToList(list types.List, key string, value attr.Value) types.List {
643656
if value.IsUnknown() || value.IsNull() {
644657
return list
645658
}
646-
val := strings.Trim(value.String(), `"`)
647-
elem := types.StringValue(fmt.Sprintf("%s=%s", key, val))
659+
var sb strings.Builder
660+
_, _ = sb.WriteString(key)
661+
_, _ = sb.WriteRune('=')
662+
_, _ = sb.WriteString(tfValueToString(value))
663+
elem := types.StringValue(sb.String())
648664
list, _ = types.ListValue(types.StringType, append(list.Elements(), elem))
649665
return list
650666
}
651667

652668
func tfListToStringSlice(l types.List) []string {
653669
var ss []string
654670
for _, el := range l.Elements() {
655-
if sv, ok := el.(stringable); !ok {
656-
panic(fmt.Sprintf("developer error: element %+v must be stringable", el))
657-
} else if sv.IsUnknown() {
658-
ss = append(ss, "")
659-
} else {
660-
ss = append(ss, sv.String())
661-
}
671+
ss = append(ss, tfValueToString(el))
662672
}
663673
return ss
664674
}

Diff for: internal/provider/cached_image_resource_test.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ func TestAccCachedImageResource(t *testing.T) {
2020
}
2121

2222
deps := setup(ctx, t, files)
23-
deps.ExtraEnv["FOO"] = "bar"
23+
deps.ExtraEnv["FOO"] = `bar
24+
baz` // THIS IS A LOAD-BEARING NEWLINE. DO NOT REMOVE.
2425
resource.Test(t, resource.TestCase{
2526
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
2627
Steps: []resource.TestStep{
@@ -42,7 +43,7 @@ func TestAccCachedImageResource(t *testing.T) {
4243
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage),
4344
// Inputs should still be present.
4445
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
45-
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"),
46+
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
4647
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
4748
// Should be empty
4849
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
@@ -63,7 +64,7 @@ func TestAccCachedImageResource(t *testing.T) {
6364
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage),
6465
// Inputs should still be present.
6566
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
66-
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"),
67+
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
6768
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
6869
// Should be empty
6970
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
@@ -81,7 +82,7 @@ func TestAccCachedImageResource(t *testing.T) {
8182
Check: resource.ComposeAggregateTestCheckFunc(
8283
// Inputs should still be present.
8384
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
84-
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"),
85+
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
8586
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
8687
// Should be empty
8788
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
@@ -92,7 +93,7 @@ func TestAccCachedImageResource(t *testing.T) {
9293
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"),
9394
resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"),
9495
resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)),
95-
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar"),
96+
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar\nbaz"),
9697
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)),
9798
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)),
9899
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"),

0 commit comments

Comments
 (0)