From 68cc59d705f407bcf0e54f98a76f7b8b3095b067 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 14 Aug 2024 14:55:22 +0100 Subject: [PATCH 1/5] 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. --- .../envbuilder_cached_image_resource.tf | 8 ++-- internal/provider/cached_image_resource.go | 42 ++++++++++++------- .../provider/cached_image_resource_test.go | 11 ++--- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf b/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf index 304aea4..af90219 100644 --- a/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf +++ b/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf @@ -66,10 +66,11 @@ resource "envbuilder_cached_image" "example" { builder_image = var.builder_image git_url = var.repo_url cache_repo = local.cache_repo + insecure = true extra_env = { "ENVBUILDER_VERBOSE" : "true" "ENVBUILDER_INSECURE" : "true" # due to local registry - "ENVBUILDER_INIT_SCRIPT" : "sleep infinity" + "ENVBUILDER_INIT_SCRIPT" : "#!/usr/bin/env bash\necho Hello && sleep infinity" "ENVBUILDER_PUSH_IMAGE" : "true" } depends_on = [docker_container.registry] @@ -77,8 +78,8 @@ resource "envbuilder_cached_image" "example" { // Run the cached image. Depending on the contents of // the cache repo, this will either be var.builder_image -// or a previously built image pusehd to var.cache_repo. -// Running `terraform apply` once (assuming empty cache) +// or a previously built image pushed to var.cache_repo. +// Running `terraform apply` once (assuming empty cache) // will result in the builder image running, and the built // image being pushed to the cache repo. // Running `terraform apply` again will result in the @@ -105,4 +106,3 @@ output "id" { output "image" { value = envbuilder_cached_image.example.image } - diff --git a/internal/provider/cached_image_resource.go b/internal/provider/cached_image_resource.go index e165453..6505736 100644 --- a/internal/provider/cached_image_resource.go +++ b/internal/provider/cached_image_resource.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -631,20 +632,35 @@ func extractEnvbuilderFromImage(ctx context.Context, imgRef, destPath string) er return fmt.Errorf("extract envbuilder binary from image %q: %w", imgRef, os.ErrNotExist) } -// NOTE: the String() method of Terraform values will evalue to `` if unknown. -// Check IsUnknown() first before calling String(). -type stringable interface { - IsUnknown() bool - IsNull() bool - String() string +// tfValueToString converts an attr.Value to its string representation +// based on its Terraform type. This is needed because the String() +// method on an attr.Value creates a 'human-readable' version of the type, which +// leads to quotes, escaped characters, and other assorted sadness. +func tfValueToString(val attr.Value) string { + if val.IsUnknown() || val.IsNull() { + return "" + } + if vs, ok := val.(interface{ ValueString() string }); ok { + return vs.ValueString() + } + if vb, ok := val.(interface{ ValueBool() bool }); ok { + return fmt.Sprintf("%t", vb.ValueBool()) + } + if vi, ok := val.(interface{ ValueInt64() int64 }); ok { + return fmt.Sprintf("%d", vi.ValueInt64()) + } + panic(fmt.Errorf("tfValueToString: value %T is not a supported type", val)) } -func appendKnownEnvToList(list types.List, key string, value stringable) types.List { +func appendKnownEnvToList(list types.List, key string, value attr.Value) types.List { if value.IsUnknown() || value.IsNull() { return list } - val := strings.Trim(value.String(), `"`) - elem := types.StringValue(fmt.Sprintf("%s=%s", key, val)) + var sb strings.Builder + _, _ = sb.WriteString(key) + _, _ = sb.WriteRune('=') + _, _ = sb.WriteString(tfValueToString(value)) + elem := types.StringValue(sb.String()) list, _ = types.ListValue(types.StringType, append(list.Elements(), elem)) return list } @@ -652,13 +668,7 @@ func appendKnownEnvToList(list types.List, key string, value stringable) types.L func tfListToStringSlice(l types.List) []string { var ss []string for _, el := range l.Elements() { - if sv, ok := el.(stringable); !ok { - panic(fmt.Sprintf("developer error: element %+v must be stringable", el)) - } else if sv.IsUnknown() { - ss = append(ss, "") - } else { - ss = append(ss, sv.String()) - } + ss = append(ss, tfValueToString(el)) } return ss } diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go index 30bfc0e..4653f8b 100644 --- a/internal/provider/cached_image_resource_test.go +++ b/internal/provider/cached_image_resource_test.go @@ -20,7 +20,8 @@ func TestAccCachedImageResource(t *testing.T) { } deps := setup(ctx, t, files) - deps.ExtraEnv["FOO"] = "bar" + deps.ExtraEnv["FOO"] = `bar +baz` // THIS IS A LOAD-BEARING NEWLINE. DO NOT REMOVE. resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ @@ -42,7 +43,7 @@ func TestAccCachedImageResource(t *testing.T) { resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), // Inputs should still be present. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), // Should be empty resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), @@ -63,7 +64,7 @@ func TestAccCachedImageResource(t *testing.T) { resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), // Inputs should still be present. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), // Should be empty resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), @@ -81,7 +82,7 @@ func TestAccCachedImageResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( // Inputs should still be present. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), // Should be empty resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), @@ -92,7 +93,7 @@ func TestAccCachedImageResource(t *testing.T) { resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar\nbaz"), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), From 9f455838b8ae33314901725c96295057d4f1e11a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 15 Aug 2024 12:58:06 +0100 Subject: [PATCH 2/5] fix(provider): correctly handle devcontainer-only in cache probe mode (#33) * chore(internal/provider): add test case for dockerfile-only operation * chore(deps): update envbuilder to include #315 --- go.mod | 4 +- go.sum | 8 +- .../provider/cached_image_resource_test.go | 215 ++++++++++-------- 3 files changed, 125 insertions(+), 102 deletions(-) diff --git a/go.mod b/go.mod index 66e2fb6..ddfc2ad 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa55 require ( github.com/GoogleContainerTools/kaniko v1.9.2 - github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29 + github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8 github.com/docker/docker v26.1.4+incompatible github.com/gliderlabs/ssh v0.3.7 github.com/go-git/go-billy/v5 v5.5.0 @@ -248,7 +248,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/go.sum b/go.sum index 4215a54..76dc58f 100644 --- a/go.sum +++ b/go.sum @@ -186,8 +186,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29 h1:PhJBofIrh6NGuTQ93nW7/KWcYX6Ju0PGoE/BbNvf87o= -github.com/coder/envbuilder v1.0.0-rc.0.0.20240807151028-6e5bfa5faa29/go.mod h1:lm33s3+chqnl7lB4avNFfDH5gXDYSunYD7/y4bJ/LMA= +github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8 h1:tfmaVV7qFpODoBliTcDQyrB09FIjCQRUtjefr7zFEXY= +github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8/go.mod h1:HFqLE6BNJhR/fLknKWon5Eqhsr5FmuEJO1OJ/RKF2BA= github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41 h1:1Ye7AcLnuT5IDv6il7Fxo+aqpzlWfedkpraCCwx8Lyo= github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= @@ -718,8 +718,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go index 4653f8b..5eeddfa 100644 --- a/internal/provider/cached_image_resource_test.go +++ b/internal/provider/cached_image_resource_test.go @@ -10,106 +10,129 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +// testEnvValue is a multi-line environment variable value that we use in +// tests to ensure that we can handle multi-line values correctly. +var testEnvValue = `bar +baz` + func TestAccCachedImageResource(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - files := map[string]string{ - ".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, - ".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest - RUN date > /date.txt`, - } - deps := setup(ctx, t, files) - deps.ExtraEnv["FOO"] = `bar -baz` // THIS IS A LOAD-BEARING NEWLINE. DO NOT REMOVE. - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Initial state: cache has not been seeded. - { - Config: deps.Config(t), - PlanOnly: true, - ExpectNonEmptyPlan: true, - }, - // Should detect that no cached image is present and plan to create the resource. - { - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Computed values MUST be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), - // Cached image should be set to the builder image. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - ), - ExpectNonEmptyPlan: true, // TODO: check the plan. - }, - // Re-running plan should have the same effect. - { - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Computed values MUST be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), - // Cached image should be set to the builder image. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - ), - ExpectNonEmptyPlan: true, // TODO: check the plan. - }, - // Now, seed the cache and re-run. We should now successfully create the cached image resource. - { - PreConfig: func() { - seedCache(ctx, t, deps) - }, - Config: deps.Config(t), - Check: resource.ComposeAggregateTestCheckFunc( - // Inputs should still be present. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), - // Should be empty - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), - // Computed - resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "id", quotedPrefix("sha256:")), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), - resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar\nbaz"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), - ), + for _, tc := range []struct { + name string + files map[string]string + }{ + { + name: "devcontainer only", + files: map[string]string{ + ".devcontainer/devcontainer.json": `{"image": "localhost:5000/test-ubuntu:latest"}`, }, - // Should produce an empty plan after apply - { - Config: deps.Config(t), - PlanOnly: true, - }, - // Ensure idempotence in this state! - { - Config: deps.Config(t), - PlanOnly: true, + }, + { + name: "devcontainer and Dockerfile", + files: map[string]string{ + ".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, + ".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest +RUN date > /date.txt`, }, }, - }) + } { + t.Run(tc.name, func(t *testing.T) { + //nolint: paralleltest + deps := setup(ctx, t, tc.files) + deps.ExtraEnv["FOO"] = testEnvValue + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Initial state: cache has not been seeded. + { + Config: deps.Config(t), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // Should detect that no cached image is present and plan to create the resource. + { + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Computed values MUST be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), + resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), + // Cached image should be set to the builder image. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + ), + ExpectNonEmptyPlan: true, // TODO: check the plan. + }, + // Re-running plan should have the same effect. + { + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Computed values MUST be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), + resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), + // Cached image should be set to the builder image. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + ), + ExpectNonEmptyPlan: true, // TODO: check the plan. + }, + // Now, seed the cache and re-run. We should now successfully create the cached image resource. + { + PreConfig: func() { + seedCache(ctx, t, deps) + }, + Config: deps.Config(t), + Check: resource.ComposeAggregateTestCheckFunc( + // Inputs should still be present. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL), + // Should be empty + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Computed + resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "id", quotedPrefix("sha256:")), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), + resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), + resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), + ), + }, + // Should produce an empty plan after apply + { + Config: deps.Config(t), + PlanOnly: true, + }, + // Ensure idempotence in this state! + { + Config: deps.Config(t), + PlanOnly: true, + }, + }, + }) + }) + } } From b55c3783a8b1dbf906c6656c8335e810340e6ffc Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 15 Aug 2024 15:49:57 +0100 Subject: [PATCH 3/5] chore(deps): update docker/docker to v26.1.5 (#36) * chore(deps): update docker/docker to v26.1.5 * also update envbuilder --- go.mod | 21 ++++++++++++--------- go.sum | 42 ++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index ddfc2ad..671c8f6 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa55 require ( github.com/GoogleContainerTools/kaniko v1.9.2 - github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8 - github.com/docker/docker v26.1.4+incompatible + github.com/coder/envbuilder v1.0.0-rc.5.0.20240815142547-cd63d0b71a40 + github.com/docker/docker v26.1.5+incompatible github.com/gliderlabs/ssh v0.3.7 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 @@ -55,8 +55,8 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.7 // indirect github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/akutz/memconn v0.1.0 // indirect @@ -100,12 +100,15 @@ require ( github.com/coder/terraform-provider-coder v0.23.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/cgroups/v3 v3.0.2 // indirect - github.com/containerd/containerd v1.7.15 // indirect + github.com/containerd/containerd v1.7.19 // indirect + github.com/containerd/containerd/api v1.7.19 // indirect github.com/containerd/continuity v0.4.3 // indirect + github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect - github.com/containerd/ttrpc v1.2.3 // indirect + github.com/containerd/ttrpc v1.2.5 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/coreos/go-oidc/v3 v3.10.0 // indirect @@ -118,7 +121,7 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.1 // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect @@ -222,7 +225,7 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect github.com/otiai10/copy v1.14.0 // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect @@ -237,7 +240,7 @@ require ( github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/v9 v9.1.0 // indirect diff --git a/go.sum b/go.sum index 76dc58f..a52cab3 100644 --- a/go.sum +++ b/go.sum @@ -77,10 +77,10 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -186,8 +186,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8 h1:tfmaVV7qFpODoBliTcDQyrB09FIjCQRUtjefr7zFEXY= -github.com/coder/envbuilder v1.0.0-rc.5.0.20240815111948-e6283db826e8/go.mod h1:HFqLE6BNJhR/fLknKWon5Eqhsr5FmuEJO1OJ/RKF2BA= +github.com/coder/envbuilder v1.0.0-rc.5.0.20240815142547-cd63d0b71a40 h1:XxZpRd+bjCAZJJHCFBVsbIrYZSYcnZBm0kp/FgfZg0o= +github.com/coder/envbuilder v1.0.0-rc.5.0.20240815142547-cd63d0b71a40/go.mod h1:m7kjZGXpP8jAZgKwfBaMbXe0bg29ERZP3g4PMpQLR4k= github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41 h1:1Ye7AcLnuT5IDv6il7Fxo+aqpzlWfedkpraCCwx8Lyo= github.com/coder/kaniko v0.0.0-20240807142221-ffc5e60fca41/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= @@ -206,18 +206,24 @@ github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaD github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= -github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= -github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= +github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE= +github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc= +github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= +github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containerd/ttrpc v1.2.3 h1:4jlhbXIGvijRtNC8F/5CpuJZ7yKOBFGFOOXg1bkISz0= -github.com/containerd/ttrpc v1.2.3/go.mod h1:ieWsXucbb8Mj9PH0rXCw1i8IunRbbAiDkpXkbfflWBM= +github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= +github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= @@ -250,10 +256,10 @@ github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2 github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= -github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= +github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= @@ -636,8 +642,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -688,8 +694,8 @@ github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5E github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= From 6cf3d934447f9dfead0fcaa9188d5017ceb42588 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 16 Aug 2024 09:42:27 +0100 Subject: [PATCH 4/5] feat(internal/provider): add env_map to cached_image_resource (#37) This PR adds `env_map` to `cached_image_resource.` This consists of the computed env in map format, which can be useful for other providers that do not expect `KEY=VALUE` format. --- docs/resources/cached_image.md | 3 +- internal/provider/cached_image_resource.go | 96 ++++++++++++------- .../provider/cached_image_resource_test.go | 52 +++++++--- 3 files changed, 105 insertions(+), 46 deletions(-) diff --git a/docs/resources/cached_image.md b/docs/resources/cached_image.md index 8498720..a7f4a38 100644 --- a/docs/resources/cached_image.md +++ b/docs/resources/cached_image.md @@ -48,7 +48,8 @@ The cached image resource can be used to retrieve a cached image produced by env ### Read-Only -- `env` (List of String, Sensitive) Computed envbuilder configuration to be set for the container. May contain secrets. +- `env` (List of String, Sensitive) Computed envbuilder configuration to be set for the container in the form of a list of strings of `key=value`. May contain secrets. +- `env_map` (Map of String, Sensitive) Computed envbuilder configuration to be set for the container in the form of a key-value map. May contain secrets. - `exists` (Boolean) Whether the cached image was exists or not for the given config. - `id` (String) Cached image identifier. This will generally be the image's SHA256 digest. - `image` (String) Outputs the cached image repo@digest if it exists, and builder image otherwise. diff --git a/internal/provider/cached_image_resource.go b/internal/provider/cached_image_resource.go index 6505736..4ea28b8 100644 --- a/internal/provider/cached_image_resource.go +++ b/internal/provider/cached_image_resource.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "sort" "strings" kconfig "github.com/GoogleContainerTools/kaniko/pkg/config" @@ -23,6 +24,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -31,6 +33,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -77,6 +80,7 @@ type CachedImageResourceModel struct { WorkspaceFolder types.String `tfsdk:"workspace_folder"` // Computed "outputs". Env types.List `tfsdk:"env"` + EnvMap types.Map `tfsdk:"env_map"` Exists types.Bool `tfsdk:"exists"` ID types.String `tfsdk:"id"` Image types.String `tfsdk:"image"` @@ -226,9 +230,8 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq }, // Computed "outputs". - // TODO(mafredri): Map vs List? Support both? "env": schema.ListAttribute{ - MarkdownDescription: "Computed envbuilder configuration to be set for the container. May contain secrets.", + MarkdownDescription: "Computed envbuilder configuration to be set for the container in the form of a list of strings of `key=value`. May contain secrets.", ElementType: types.StringType, Computed: true, Sensitive: true, @@ -236,6 +239,15 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq listplanmodifier.RequiresReplace(), }, }, + "env_map": schema.MapAttribute{ + MarkdownDescription: "Computed envbuilder configuration to be set for the container in the form of a key-value map. May contain secrets.", + ElementType: types.StringType, + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, "exists": schema.BoolAttribute{ MarkdownDescription: "Whether the cached image was exists or not for the given config.", Computed: true, @@ -338,28 +350,36 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest data.Exists = types.BoolValue(true) // Set the expected environment variables. + env := make(map[string]string) for key, elem := range data.ExtraEnv.Elements() { - data.Env = appendKnownEnvToList(data.Env, key, elem) + env[key] = tfValueToString(elem) } - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_REPO", data.CacheRepo) - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_URL", data.GitURL) + env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo) + env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL) + if !data.CacheTTLDays.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_TTL_DAYS", data.CacheTTLDays) + env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays) } if !data.GitUsername.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_USERNAME", data.GitUsername) + env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername) } if !data.GitPassword.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_PASSWORD", data.GitPassword) + env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword) } // Default to remote build mode. if data.RemoteRepoBuildMode.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", types.BoolValue(true)) + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true" } else { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", data.RemoteRepoBuildMode) + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode) } + var diag diag.Diagnostics + data.EnvMap, diag = basetypes.NewMapValueFrom(ctx, types.StringType, env) + resp.Diagnostics.Append(diag...) + data.Env, diag = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env)) + resp.Diagnostics.Append(diag...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -396,29 +416,36 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq data.ID = types.StringValue(digest.String()) } // Compute the env attribute from the config map. - // TODO(mafredri): Convert any other relevant attributes given via schema. + env := make(map[string]string) for key, elem := range data.ExtraEnv.Elements() { - data.Env = appendKnownEnvToList(data.Env, key, elem) + env[key] = tfValueToString(elem) } - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_REPO", data.CacheRepo) - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_URL", data.GitURL) + env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo) + env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL) + if !data.CacheTTLDays.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_CACHE_TTL_DAYS", data.CacheTTLDays) + env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays) } if !data.GitUsername.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_USERNAME", data.GitUsername) + env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername) } if !data.GitPassword.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_GIT_PASSWORD", data.GitPassword) + env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword) } // Default to remote build mode. if data.RemoteRepoBuildMode.IsNull() { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", types.BoolValue(true)) + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true" } else { - data.Env = appendKnownEnvToList(data.Env, "ENVBUILDER_REMOTE_REPO_BUILD_MODE", data.RemoteRepoBuildMode) + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode) } + var diag diag.Diagnostics + data.EnvMap, diag = basetypes.NewMapValueFrom(ctx, types.StringType, env) + resp.Diagnostics.Append(diag...) + data.Env, diag = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env)) + resp.Diagnostics.Append(diag...) + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -652,19 +679,8 @@ func tfValueToString(val attr.Value) string { panic(fmt.Errorf("tfValueToString: value %T is not a supported type", val)) } -func appendKnownEnvToList(list types.List, key string, value attr.Value) types.List { - if value.IsUnknown() || value.IsNull() { - return list - } - var sb strings.Builder - _, _ = sb.WriteString(key) - _, _ = sb.WriteRune('=') - _, _ = sb.WriteString(tfValueToString(value)) - elem := types.StringValue(sb.String()) - list, _ = types.ListValue(types.StringType, append(list.Elements(), elem)) - return list -} - +// tfListToStringSlice converts a types.List to a []string by calling +// tfValueToString on each element. func tfListToStringSlice(l types.List) []string { var ss []string for _, el := range l.Elements() { @@ -692,3 +708,19 @@ func tfLogFunc(ctx context.Context) eblog.Func { logFn(ctx, fmt.Sprintf(format, args...)) } } + +// sortedKeyValues returns the keys and values of the map in the form "key=value" +// sorted by key in lexicographical order. +func sortedKeyValues(m map[string]string) []string { + pairs := make([]string, 0, len(m)) + var sb strings.Builder + for k := range m { + _, _ = sb.WriteString(k) + _, _ = sb.WriteRune('=') + _, _ = sb.WriteString(m[k]) + pairs = append(pairs, sb.String()) + sb.Reset() + } + sort.Strings(pairs) + return pairs +} diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go index 5eeddfa..929c7b0 100644 --- a/internal/provider/cached_image_resource_test.go +++ b/internal/provider/cached_image_resource_test.go @@ -24,12 +24,18 @@ func TestAccCachedImageResource(t *testing.T) { files map[string]string }{ { + // This test case is the simplest possible case: a devcontainer.json. + // However, it also makes sure we are able to generate a Dockerfile + // from the devcontainer.json. name: "devcontainer only", files: map[string]string{ ".devcontainer/devcontainer.json": `{"image": "localhost:5000/test-ubuntu:latest"}`, }, }, { + // This test case includes a Dockerfile in addition to the devcontainer.json. + // The Dockerfile writes the current date to a file. This is currently not checked but + // illustrates that a RUN instruction is cached. name: "devcontainer and Dockerfile", files: map[string]string{ ".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`, @@ -46,20 +52,19 @@ RUN date > /date.txt`, resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ - // Initial state: cache has not been seeded. + // 1) Initial state: cache has not been seeded. { Config: deps.Config(t), PlanOnly: true, ExpectNonEmptyPlan: true, }, - // Should detect that no cached image is present and plan to create the resource. + // 2) Should detect that no cached image is present and plan to create the resource. { Config: deps.Config(t), Check: resource.ComposeAggregateTestCheckFunc( // Computed values MUST be present. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), // Cached image should be set to the builder image. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), // Inputs should still be present. @@ -70,17 +75,18 @@ RUN date > /date.txt`, resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Environment variables + assertEnv(t, deps), ), ExpectNonEmptyPlan: true, // TODO: check the plan. }, - // Re-running plan should have the same effect. + // 3) Re-running plan should have the same effect. { Config: deps.Config(t), Check: resource.ComposeAggregateTestCheckFunc( // Computed values MUST be present. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "id", uuid.Nil.String()), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "false"), - resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "env.0"), // Cached image should be set to the builder image. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage), // Inputs should still be present. @@ -91,10 +97,12 @@ RUN date > /date.txt`, resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"), resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"), resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"), + // Environment variables + assertEnv(t, deps), ), ExpectNonEmptyPlan: true, // TODO: check the plan. }, - // Now, seed the cache and re-run. We should now successfully create the cached image resource. + // 4) Now, seed the cache and re-run. We should now successfully create the cached image resource. { PreConfig: func() { seedCache(ctx, t, deps) @@ -114,19 +122,16 @@ RUN date > /date.txt`, resource.TestCheckResourceAttr("envbuilder_cached_image.test", "exists", "true"), resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"), resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", "FOO=bar\nbaz"), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), + // Environment variables + assertEnv(t, deps), ), }, - // Should produce an empty plan after apply + // 5) Should produce an empty plan after apply { Config: deps.Config(t), PlanOnly: true, }, - // Ensure idempotence in this state! + // 6) Ensure idempotence in this state! { Config: deps.Config(t), PlanOnly: true, @@ -136,3 +141,24 @@ RUN date > /date.txt`, }) } } + +// assertEnv is a test helper that checks the environment variables set on the +// cached image resource based on the provided test dependencies. +func assertEnv(t *testing.T, deps testDependencies) resource.TestCheckFunc { + t.Helper() + return resource.ComposeAggregateTestCheckFunc( + // Check that the environment variables are set correctly. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), + // Check that the extra environment variables are set correctly. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "FOO=bar\nbaz"), + // We should not have any other environment variables set. + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), + // Check that the same values are set in env_map. + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.FOO", "bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_CACHE_REPO", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_GIT_URL", deps.Repo.URL), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true"), + ) +} From cd1599f79d0417d3dc09ac16616e7d727cdb6ffe Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 16 Aug 2024 14:55:46 +0100 Subject: [PATCH 5/5] fix(internal/provider): set all supported envbuilder options (#38) * fix(internal/provider): set all supported envbuilder options * do not allow overriding git_url and cache_repo --- internal/provider/cached_image_resource.go | 193 ++++++++++++------ .../provider/cached_image_resource_test.go | 17 +- 2 files changed, 146 insertions(+), 64 deletions(-) diff --git a/internal/provider/cached_image_resource.go b/internal/provider/cached_image_resource.go index 4ea28b8..dbd5500 100644 --- a/internal/provider/cached_image_resource.go +++ b/internal/provider/cached_image_resource.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -160,7 +161,6 @@ func (r *CachedImageResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "(Envbuilder option) Terminates upon a build failure. This is handy when preferring the FALLBACK_IMAGE in cases where no devcontainer.json or image is provided. However, it ensures that the container stops if the build process encounters an error.", Optional: true, }, - // TODO(mafredri): Map vs List? Support both? "extra_env": schema.MapAttribute{ MarkdownDescription: "Extra environment variables to set for the container. This may include envbuilder options.", ElementType: types.StringType, @@ -293,6 +293,135 @@ func (r *CachedImageResource) Configure(ctx context.Context, req resource.Config r.client = client } +// setComputedEnv sets data.Env and data.EnvMap based on the values of the +// other fields in the model. +func (data *CachedImageResourceModel) setComputedEnv(ctx context.Context) diag.Diagnostics { + env := make(map[string]string) + + env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo) + env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL) + + if !data.BaseImageCacheDir.IsNull() { + env["ENVBUILDER_BASE_IMAGE_CACHE_DIR"] = tfValueToString(data.BaseImageCacheDir) + } + + if !data.BuildContextPath.IsNull() { + env["ENVBUILDER_BUILD_CONTEXT_PATH"] = tfValueToString(data.BuildContextPath) + } + + if !data.CacheTTLDays.IsNull() { + env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays) + } + + if !data.DevcontainerDir.IsNull() { + env["ENVBUILDER_DEVCONTAINER_DIR"] = tfValueToString(data.DevcontainerDir) + } + + if !data.DevcontainerJSONPath.IsNull() { + env["ENVBUILDER_DEVCONTAINER_JSON_PATH"] = tfValueToString(data.DevcontainerJSONPath) + } + + if !data.DockerfilePath.IsNull() { + env["ENVBUILDER_DOCKERFILE_PATH"] = tfValueToString(data.DockerfilePath) + } + + if !data.DockerConfigBase64.IsNull() { + env["ENVBUILDER_DOCKER_CONFIG_BASE64"] = tfValueToString(data.DockerConfigBase64) + } + + if !data.ExitOnBuildFailure.IsNull() { + env["ENVBUILDER_EXIT_ON_BUILD_FAILURE"] = tfValueToString(data.ExitOnBuildFailure) + } + + if !data.FallbackImage.IsNull() { + env["ENVBUILDER_FALLBACK_IMAGE"] = tfValueToString(data.FallbackImage) + } + + if !data.GitCloneDepth.IsNull() { + env["ENVBUILDER_GIT_CLONE_DEPTH"] = tfValueToString(data.GitCloneDepth) + } + + if !data.GitCloneSingleBranch.IsNull() { + env["ENVBUILDER_GIT_CLONE_SINGLE_BRANCH"] = tfValueToString(data.GitCloneSingleBranch) + } + + if !data.GitHTTPProxyURL.IsNull() { + env["ENVBUILDER_GIT_HTTP_PROXY_URL"] = tfValueToString(data.GitHTTPProxyURL) + } + + if !data.GitSSHPrivateKeyPath.IsNull() { + env["ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH"] = tfValueToString(data.GitSSHPrivateKeyPath) + } + + if !data.GitUsername.IsNull() { + env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername) + } + + if !data.GitPassword.IsNull() { + env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword) + } + + if !data.IgnorePaths.IsNull() { + env["ENVBUILDER_IGNORE_PATHS"] = strings.Join(tfListToStringSlice(data.IgnorePaths), ",") + } + + if !data.Insecure.IsNull() { + env["ENVBUILDER_INSECURE"] = tfValueToString(data.Insecure) + } + + // Default to remote build mode. + if data.RemoteRepoBuildMode.IsNull() { + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true" + } else { + env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode) + } + + if !data.SSLCertBase64.IsNull() { + env["ENVBUILDER_SSL_CERT_BASE64"] = tfValueToString(data.SSLCertBase64) + } + + if !data.Verbose.IsNull() { + env["ENVBUILDER_VERBOSE"] = tfValueToString(data.Verbose) + } + + if !data.WorkspaceFolder.IsNull() { + env["ENVBUILDER_WORKSPACE_FOLDER"] = tfValueToString(data.WorkspaceFolder) + } + + // Do ExtraEnv last so that it can override any other values. + // With one exception: ENVBUILDER_CACHE_REPO and ENVBUILDER_GIT_URL are required and should not be overridden. + // Other values set by the provider may be overridden, but will generate a warning. + var diag, ds diag.Diagnostics + if !data.ExtraEnv.IsNull() { + for key, elem := range data.ExtraEnv.Elements() { + switch key { + // These are required and should not be overridden. + case "ENVBUILDER_CACHE_REPO", "ENVBUILDER_GIT_URL": + diag.AddAttributeWarning(path.Root("extra_env"), + "Cannot override required environment variable", + fmt.Sprintf("The key %q in extra_env cannot be overridden.", key), + ) + default: + if _, ok := env[key]; ok { + // This is a warning because it's possible that the user wants to override + // a value set by the provider. + diag.AddAttributeWarning(path.Root("extra_env"), + "Overriding provider environment variable", + fmt.Sprintf("The key %q in extra_env overrides an environment variable set by the provider.", key), + ) + } + env[key] = tfValueToString(elem) + } + } + } + + data.EnvMap, ds = basetypes.NewMapValueFrom(ctx, types.StringType, env) + diag = append(diag, ds...) + data.Env, ds = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env)) + diag = append(diag, ds...) + return diag +} + func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data CachedImageResourceModel @@ -350,35 +479,7 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest data.Exists = types.BoolValue(true) // Set the expected environment variables. - env := make(map[string]string) - for key, elem := range data.ExtraEnv.Elements() { - env[key] = tfValueToString(elem) - } - - env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo) - env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL) - - if !data.CacheTTLDays.IsNull() { - env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays) - } - if !data.GitUsername.IsNull() { - env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername) - } - if !data.GitPassword.IsNull() { - env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword) - } - // Default to remote build mode. - if data.RemoteRepoBuildMode.IsNull() { - env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true" - } else { - env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode) - } - - var diag diag.Diagnostics - data.EnvMap, diag = basetypes.NewMapValueFrom(ctx, types.StringType, env) - resp.Diagnostics.Append(diag...) - data.Env, diag = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env)) - resp.Diagnostics.Append(diag...) + resp.Diagnostics.Append(data.setComputedEnv(ctx)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -415,36 +516,9 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq data.Image = types.StringValue(fmt.Sprintf("%s@%s", data.CacheRepo.ValueString(), digest)) data.ID = types.StringValue(digest.String()) } - // Compute the env attribute from the config map. - env := make(map[string]string) - for key, elem := range data.ExtraEnv.Elements() { - env[key] = tfValueToString(elem) - } - env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo) - env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL) - - if !data.CacheTTLDays.IsNull() { - env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays) - } - if !data.GitUsername.IsNull() { - env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername) - } - if !data.GitPassword.IsNull() { - env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword) - } - // Default to remote build mode. - if data.RemoteRepoBuildMode.IsNull() { - env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true" - } else { - env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode) - } - - var diag diag.Diagnostics - data.EnvMap, diag = basetypes.NewMapValueFrom(ctx, types.StringType, env) - resp.Diagnostics.Append(diag...) - data.Env, diag = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env)) - resp.Diagnostics.Append(diag...) + // Set the expected environment variables. + resp.Diagnostics.Append(data.setComputedEnv(ctx)...) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -556,6 +630,7 @@ func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImag Insecure: data.Insecure.ValueBool(), // might have internal CAs? IgnorePaths: tfListToStringSlice(data.IgnorePaths), // may need to be specified? // The below options are not relevant and are set to their zero value explicitly. + // They must be set by extra_env. CoderAgentSubsystem: nil, CoderAgentToken: "", CoderAgentURL: "", diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go index 929c7b0..c4f3f9a 100644 --- a/internal/provider/cached_image_resource_test.go +++ b/internal/provider/cached_image_resource_test.go @@ -48,6 +48,8 @@ RUN date > /date.txt`, //nolint: paralleltest deps := setup(ctx, t, tc.files) deps.ExtraEnv["FOO"] = testEnvValue + deps.ExtraEnv["ENVBUILDER_GIT_URL"] = "https://not.the.real.git/url" + deps.ExtraEnv["ENVBUILDER_CACHE_REPO"] = "not-the-real-cache-repo" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -149,16 +151,21 @@ func assertEnv(t *testing.T, deps testDependencies) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( // Check that the environment variables are set correctly. resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH=%s", deps.Repo.Key)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.4", "ENVBUILDER_VERBOSE=true"), // Check that the extra environment variables are set correctly. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "FOO=bar\nbaz"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.5", "FOO=bar\nbaz"), // We should not have any other environment variables set. - resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.4"), + resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.6"), + // Check that the same values are set in env_map. - resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.FOO", "bar\nbaz"), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_CACHE_REPO", deps.CacheRepo), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_GIT_URL", deps.Repo.URL), resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_VERBOSE", "true"), + resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.FOO", "bar\nbaz"), ) }