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
66 changes: 66 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,72 @@ func Test_Extract(t *testing.T) {
dir: "badparam",
failPreview: true,
},
{
// Parameters, presets, and tags whose values flow from resource
// blocks. This pins the baseline so that any evaluation strategy
// that skips resources (see coder/trivy#74) must keep every
// reference shape here: direct, via local, count and for_each
// index, transitive resource->resource, conditional, try(),
// nested option and dynamic blocks, meta-arguments, data-source
// intermediaries, and module inputs. An orphan resource nothing
// references may be dropped 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
"derived": "derived-large", // tag -> resource -> parameter
},
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

// Expression shapes the pruner must see through.
"picked": ap().value("alt-large").def("alt-large"), // conditional
"tried": ap().value("alt-large").def("alt-large"), // try()
"varindexed": ap().value("poolimg").def("poolimg"), // index is an expression

// References inside nested blocks.
"staticopt": ap().value("large").def("large").optVals("large"),
"dynopts": ap().value("poolimg").def("poolimg").optVals("poolimg"),

// Meta-argument on the target reads a resource; the param only
// exists if that resource survived.
"gated": ap().value("on").def("on"),

// Non-target data source as the intermediary.
"viadata": ap().value("large").def("large"),

// Module input carries a root resource in; module-owned resource
// is never pruned.
"viamodule": ap().value("large").def("large"),
"modresource": ap().value("sub-large").def("sub-large"),
},
presets: map[string]assertPreset{
// The preset independently keeps a resource nothing else references.
"big": aPre().value("flavor", "preset-large"),
// Nested prebuilds block reads a resource.
"pre": aPre().value("flavor", "large").prebuildCount(2),
},
variables: map[string]assertVariable{
"pick_alt": av().def(cty.BoolVal(true)).typeEq(cty.Bool),
"idx": av().def(cty.NumberIntVal(1)).typeEq(cty.Number),
},
},
{
// JSON syntax templates. A single JSON expression that references two
// resources must keep both; the parameter's default is the same as
// for the HCL equivalent.
name: "resource closure json",
dir: "resourceclosurejson",
expTags: map[string]string{},
params: map[string]assertParam{
"joined": ap().value("A-B").def("A-B"),
},
},
{
name: "sometags",
dir: "sometags",
Expand Down
239 changes: 239 additions & 0 deletions testdata/resourceclosure/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// 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"
}

// --- Reference shapes beyond a plain attribute traversal ------------------
// Each of these reaches a resource through an expression form the pruner has
// to see through. If any is missed, the parameter default goes unknown.

// Conditional and try(): both operands are references.
variable "pick_alt" {
type = bool
default = true
}

resource "docker_image" "alt" {
name = "alt-large"
}

data "coder_parameter" "picked" {
name = "picked"
type = "string"
default = var.pick_alt ? docker_image.alt.name : docker_image.base.name
}

data "coder_parameter" "tried" {
name = "tried"
type = "string"
default = try(docker_image.alt.name, "fallback")
}

// Index that is itself an expression, not a literal.
variable "idx" {
type = number
default = 1
}

data "coder_parameter" "varindexed" {
name = "varindexed"
type = "string"
default = docker_image.pool[var.idx].name
}

// --- References inside nested blocks ----------------------------------------
// A static option block and a dynamic option block both reach a resource.
data "coder_parameter" "staticopt" {
name = "staticopt"
type = "string"
form_type = "dropdown"
default = docker_image.base.name
option {
name = docker_image.base.name
value = docker_image.base.name
}
}

data "coder_parameter" "dynopts" {
name = "dynopts"
type = "string"
form_type = "dropdown"
default = "poolimg"
dynamic "option" {
for_each = toset(docker_image.pool[*].name)
content {
name = option.value
value = option.value
}
}
}

// --- Meta-argument on a target reads a resource -----------------------------
resource "docker_image" "gate" {
name = "gate"
}

data "coder_parameter" "gated" {
count = docker_image.gate.name == "gate" ? 1 : 0
name = "gated"
type = "string"
default = "on"
}

// --- Data source as intermediary: param -> data -> resource -----------------
data "docker_registry_image" "viaresource" {
name = docker_image.base.name
}

data "coder_parameter" "viadata" {
name = "viadata"
type = "string"
default = data.docker_registry_image.viaresource.name
}

// --- Tag -> resource -> parameter -------------------------------------------
// The common direction (resource reads a parameter) combined with the resource
// also being in a target's closure. It must be kept and evaluate after the
// parameter it reads.
resource "docker_image" "fromparam" {
name = "derived-${data.coder_parameter.flavor.value}"
}

data "coder_workspace_tags" "derived" {
tags = {
derived = docker_image.fromparam.name
}
}

// --- Preset nested block reads a resource -----------------------------------
data "coder_workspace_preset" "pre" {
name = "pre"
parameters = {
flavor = "large"
}
prebuilds {
instances = length(docker_image.pool)
}
}

// --- Module: root resource flows in through an input; the module's own
// resource is untouched by pruning -------------------------------------------
module "sub" {
source = "./submodule"
image_name = docker_image.base.name
}

data "coder_parameter" "viamodule" {
name = "viamodule"
type = "string"
default = module.sub.image_name
}

data "coder_parameter" "modresource" {
name = "modresource"
type = "string"
default = module.sub.internal_name
}
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
17 changes: 17 additions & 0 deletions testdata/resourceclosure/submodule/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Submodules are never pruned; only root resources are. This module receives a
// root resource's value through an input and also owns a resource of its own.
variable "image_name" {
type = string
}

resource "docker_image" "internal" {
name = "sub-${var.image_name}"
}

output "image_name" {
value = var.image_name
}

output "internal_name" {
value = docker_image.internal.name
}
26 changes: 26 additions & 0 deletions testdata/resourceclosurejson/main.tf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"terraform": {
"required_providers": {
"coder": { "source": "coder/coder", "version": "2.4.0-pre0" },
"docker": { "source": "kreuzwerker/docker", "version": "3.0.2" }
}
},
"resource": {
"docker_image": {
"first": { "name": "A" },
"second": { "name": "B" }
}
},
"locals": {
"joined": "${docker_image.first.name}-${docker_image.second.name}"
},
"data": {
"coder_parameter": {
"joined": {
"name": "joined",
"type": "string",
"default": "${local.joined}"
}
}
}
}
1 change: 1 addition & 0 deletions testdata/resourceclosurejson/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
Loading