diff --git a/go.mod b/go.mod index 5e2285e..b433416 100644 --- a/go.mod +++ b/go.mod @@ -159,4 +159,4 @@ require ( // Trivy has some issues that we're floating patches for, and will hopefully // be upstreamed eventually. -replace github.com/aquasecurity/trivy => github.com/coder/trivy v0.0.0-20260309164037-c413f5a2f511 +replace github.com/aquasecurity/trivy => github.com/PushTheLimit/trivy v0.0.0-20260902202626-5f70611adf65 diff --git a/go.sum b/go.sum index 1b7c977..5288d7b 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/PushTheLimit/trivy v0.0.0-20260902202626-5f70611adf65 h1:fVQjYtdo5NMXre7YRkxb6M2vLRjnIVSmRkzdN446KBM= +github.com/PushTheLimit/trivy v0.0.0-20260902202626-5f70611adf65/go.mod h1:+zF17ZBOdhFWwD3+GkLxZ/vkmKLudoOtt+hgnc1TQpA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= @@ -115,8 +117,6 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= github.com/coder/terraform-provider-coder/v2 v2.8.0 h1:pbWfegCPI0v8eATgE8kGwIyuaMPgMRIcdLF2GTVkgG0= github.com/coder/terraform-provider-coder/v2 v2.8.0/go.mod h1:WrdLSbihuzH1RZhwrU+qmkqEhUbdZT/sjHHdarm5b5g= -github.com/coder/trivy v0.0.0-20260309164037-c413f5a2f511 h1:wJS3Pk13VuCbV8hjrQRnOBCUwP3Islk91sMvbSdY0Vk= -github.com/coder/trivy v0.0.0-20260309164037-c413f5a2f511/go.mod h1:+zF17ZBOdhFWwD3+GkLxZ/vkmKLudoOtt+hgnc1TQpA= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/preview.go b/preview.go index 32ac43f..f41e08b 100644 --- a/preview.go +++ b/preview.go @@ -243,6 +243,16 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn parser.OptionWithEvalHook(ownerHook), parser.OptionWithWorkingDirectoryPath("/"), parser.OptionWithEvalHook(parameterContextsEvalHook(input)), + // Only the parameter/preset/tag blocks and what they reference need to be + // evaluated to render a workspace form. The resources a workspace would + // create cannot feed those blocks, so pruning the ones nothing references + // avoids evaluating the entire module graph on every request without + // changing any parameter, preset or tag. See OptionWithResourceClosure. + parser.OptionWithResourceClosure([]string{ + "coder_parameter", + "coder_workspace_preset", + "coder_workspace_tags", + }), // 'OptionsWithTfVars' cannot be set with 'OptionWithTFVarsPaths'. So load the // tfvars from the files ourselves and merge with the user-supplied tf vars. parser.OptionsWithTfVars(variableValues), diff --git a/preview_test.go b/preview_test.go index 49a7fb6..d578b8c 100644 --- a/preview_test.go +++ b/preview_test.go @@ -54,6 +54,96 @@ func Test_Extract(t *testing.T) { dir: "badparam", failPreview: true, }, + { + // Parameters, presets, and tags whose values flow from resource + // blocks must be unchanged when unreferenced resources are pruned by + // the target closure. Each target type (param/preset/tag) is proven + // to independently keep a resource, across direct, local, count-index, + // for_each-index, and transitive resource->resource references. An + // orphan resource is pruned without effect. + name: "resource closure", + dir: "resourceclosure", + expTags: map[string]string{ + "flavor": "large", // tag reads a param-shared resource + "tagged": "tag-large", // tag independently keeps its own resource + }, + params: map[string]assertParam{ + "flavor": ap().value("large").def("large"), // via local + "direct": ap().value("large").def("large"), // direct + "indexed": ap().value("poolimg").def("poolimg"), // count index + "byeach": ap().value("fe-large").def("fe-large"), // for_each index + "chained": ap().value("chain-large").def("chain-large"), // transitive resource->resource + }, + presets: map[string]assertPreset{ + // The preset independently keeps a resource nothing else references. + "big": aPre().value("flavor", "preset-large"), + }, + }, + { + // A count-guarded parameter and a tag both driven by a submodule + // output. + name: "count-guarded param from submodule output", + dir: "countsub", + expTags: map[string]string{ + "test": "true", + }, + unknownTags: []string{}, + params: map[string]assertParam{ + "Region": ap().value("STATIC").def("STATIC"), + }, + }, + { + // A tag whose value comes from an http data source that cannot be + // resolved without plan data, so it is unknown. + name: "tag from unknown http data source", + dir: "http", + expTags: map[string]string{}, + unknownTags: []string{"tfversion"}, + }, + { + // Non-string tag keys and values. + name: "non-string tag values", + dir: "notstringtag", + expTags: map[string]string{ + "zone": "5", + "10": "hello", + }, + unknownTags: []string{}, + }, + { + // No guesses entered, so every parameter's value and default is the + // empty string. + name: "wordle empty defaults", + dir: "wordle", + params: map[string]assertParam{ + "letter_bank": ap().value("").def(""), + "one": ap().value("").def(""), + "two": ap().value("").def(""), + "three": ap().value("").def(""), + "four": ap().value("").def(""), + "five": ap().value("").def(""), + "six": ap().value("").def(""), + }, + }, + { + // Each "*_format" dropdown drives the sibling parameter's form_type, + // so this exercises form_type resolved from another parameter's value + // alongside static defaults. like_it defaults false, so the + // count-guarded "satisfaction" parameter is absent. + name: "form types", + dir: "formtypes", + params: map[string]assertParam{ + "single_select": ap().value("radio").def("radio").formType(provider.ParameterFormTypeDropdown), + "single": ap().value("alpha-value").def("alpha-value").formType(provider.ParameterFormTypeRadio), + "number_format": ap().value("input").def("input").formType(provider.ParameterFormTypeDropdown), + "number": ap().value("7").def("7").formType(provider.ParameterFormTypeInput), + "boolean_format": ap().value("radio").def("radio").formType(provider.ParameterFormTypeDropdown), + "boolean": ap().value("true").def("true").formType(provider.ParameterFormTypeRadio), + "list_format": ap().value("multi-select").def("multi-select").formType(provider.ParameterFormTypeDropdown), + "list": ap().value(`["blue","green"]`).def(`["blue","green"]`).formType(provider.ParameterFormTypeMultiSelect), + "like_it": ap().value("false").def("false").formType(provider.ParameterFormTypeCheckbox), + }, + }, { name: "sometags", dir: "sometags", diff --git a/testdata/resourceclosure/main.tf b/testdata/resourceclosure/main.tf new file mode 100644 index 0000000..7622cfa --- /dev/null +++ b/testdata/resourceclosure/main.tf @@ -0,0 +1,107 @@ +// Exercises target-driven resource-closure pruning end to end. Every resource +// below is reachable from exactly one target (a parameter, a preset, or a tag) +// or through a resource->resource chain, so each proves that its path keeps the +// resource alive through pruning. The orphan is reachable from nothing and is +// dropped; it must not change any output. Resource attributes use static values +// so the assertions isolate pruning behaviour, not count/for_each value binding. +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + } +} + +// --- Parameter-reached resources ------------------------------------------- +resource "docker_image" "base" { + name = "large" +} + +resource "docker_image" "pool" { + count = 2 + name = "poolimg" +} + +resource "docker_image" "bykey" { + for_each = toset(["a"]) + name = "fe-large" +} + +// Transitive chain: a parameter reaches chain_a directly, chain_a reaches +// chain_b. Pruning must keep both. +resource "docker_image" "chain_b" { + name = "chain-large" +} + +resource "docker_image" "chain_a" { + name = docker_image.chain_b.name +} + +locals { + flavor = docker_image.base.name +} + +data "coder_parameter" "flavor" { + name = "flavor" + type = "string" + default = local.flavor +} + +data "coder_parameter" "direct" { + name = "direct" + type = "string" + default = docker_image.base.name +} + +data "coder_parameter" "indexed" { + name = "indexed" + type = "string" + default = docker_image.pool[0].name +} + +data "coder_parameter" "byeach" { + name = "byeach" + type = "string" + default = docker_image.bykey["a"].name +} + +data "coder_parameter" "chained" { + name = "chained" + type = "string" + default = docker_image.chain_a.name +} + +// --- Preset-reached resource (nothing else references it) ------------------ +resource "docker_image" "preset_only" { + name = "preset-large" +} + +data "coder_workspace_preset" "big" { + name = "big" + parameters = { + flavor = docker_image.preset_only.name + } +} + +// --- Tag-reached resource (nothing else references it) --------------------- +resource "docker_image" "tag_only" { + name = "tag-large" +} + +data "coder_workspace_tags" "tags" { + tags = { + flavor = docker_image.base.name + tagged = docker_image.tag_only.name + } +} + +// --- Orphan: reachable from no target, pruned, must not affect outputs ----- +resource "docker_container" "orphan" { + name = "orphan" + image = "does-not-exist" +} diff --git a/testdata/resourceclosure/skipe2e b/testdata/resourceclosure/skipe2e new file mode 100644 index 0000000..5e15859 --- /dev/null +++ b/testdata/resourceclosure/skipe2e @@ -0,0 +1 @@ +closure logic is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here