Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
10 changes: 10 additions & 0 deletions preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
90 changes: 90 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 107 additions & 0 deletions testdata/resourceclosure/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions testdata/resourceclosure/skipe2e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
closure logic is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here