diff --git a/preview_test.go b/preview_test.go index dc3598e..5f360e4 100644 --- a/preview_test.go +++ b/preview_test.go @@ -180,6 +180,68 @@ func Test_Extract(t *testing.T) { "ide_selector": ap().value(`["GoLand"]`), }, }, + { + // Second batch of resource-reachability vectors; see the fixture + // header for the evaluation paths covered. Complements + // "resource closure". + name: "resource closure 2", + dir: "resourceclosure2", + expTags: map[string]string{ + "pool": "pool-0,pool-1", // splat directly in a tag + "shards": "3", // resource for_each driven by a parameter + }, + params: map[string]assertParam{ + "validated": ap().value("large").def("large"), // validation block reads a resource + "forlist": ap().value("pool-0,pool-1").def("pool-0,pool-1"), // [for ...] over a count resource + "formap": ap().value("img-b").def("img-b"), // {for ...} over a for_each resource + "fallback": ap().value("fallback").def("fallback"), // count = 0 resource, try() fallback + "shards": ap().value(`["x","y","z"]`), + "nested": ap().value("inner-outer-large").def("inner-outer-large"), // two-level module chain + "hops": ap().value("large").def("large"), // multi-hop locals + // A computed attribute is unknown whether or not the resource is + // evaluated. Pins that it stays unknown and does not become an + // error. + "computed": ap().unknown(), + }, + }, + { + // Same fixture with an input that fails the validation block. The + // error text is built from a resource attribute, so it must still + // resolve. + name: "resource closure 2 invalid input", + dir: "resourceclosure2", + input: preview.Input{ + ParameterValues: map[string]string{ + "validated": "small", + }, + }, + expTags: map[string]string{ + "pool": "pool-0,pool-1", + "shards": "3", + }, + params: map[string]assertParam{ + "validated": apWithDiags().value("small").def("large"). + errorDiagnostics("must be large"), + "forlist": ap().value("pool-0,pool-1"), + "formap": ap().value("img-b"), + "fallback": ap().value("fallback"), + "shards": ap().value(`["x","y","z"]`), + "nested": ap().value("inner-outer-large"), + "hops": ap().value("large"), + "computed": ap().unknown(), + }, + }, + { + // The only parameter is in a submodule; the root has no parameter, + // preset, or tag. A root resource it reads through a module input + // must still be evaluated. + name: "resource closure submodule only", + dir: "resourceclosuresubmod", + expTags: map[string]string{}, + params: map[string]assertParam{ + "flavor": ap().value("large").def("large"), + }, + }, { name: "sometags", dir: "sometags", diff --git a/testdata/resourceclosure2/main.tf b/testdata/resourceclosure2/main.tf new file mode 100644 index 0000000..fce45b3 --- /dev/null +++ b/testdata/resourceclosure2/main.tf @@ -0,0 +1,134 @@ +// Second batch of resource-reachability vectors (see resourceclosure for the +// first). Each parameter, preset, or tag below reads a resource through an +// evaluation path that is distinct from the first batch: validation blocks, +// for expressions, a splat outside a dynamic block, a count = 0 resource, a +// resource whose for_each is driven by a parameter, a computed attribute, a +// two-level module chain, and multi-hop locals. Any strategy that skips +// resources must leave every value here unchanged. +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + } +} + +resource "docker_image" "base" { + name = "large" +} + +resource "docker_image" "pool" { + count = 2 + name = "pool-${count.index}" +} + +resource "docker_image" "bykey" { + for_each = toset(["a", "b"]) + name = "img-${each.key}" +} + +// --- validation block reads a resource ------------------------------------- +data "coder_parameter" "validated" { + name = "validated" + type = "string" + default = docker_image.base.name + validation { + regex = "^${docker_image.base.name}$" + error = "must be ${docker_image.base.name}" + } +} + +// --- for expressions over resources ---------------------------------------- +locals { + pool_names = [for i in docker_image.pool : i.name] + bykey_map = { for k, v in docker_image.bykey : k => v.name } +} + +data "coder_parameter" "forlist" { + name = "forlist" + type = "string" + default = join(",", local.pool_names) +} + +data "coder_parameter" "formap" { + name = "formap" + type = "string" + default = local.bykey_map["b"] +} + +// --- splat directly in a tag ------------------------------------------------ +data "coder_workspace_tags" "tags" { + tags = { + pool = join(",", docker_image.pool[*].name) + // A resource whose for_each is driven by a parameter (below). It both + // reads a parameter and feeds a target. + shards = tostring(length(docker_image.shard)) + } +} + +// --- count = 0 resource: kept, expands to nothing --------------------------- +resource "docker_image" "none" { + count = 0 + name = "never" +} + +data "coder_parameter" "fallback" { + name = "fallback" + type = "string" + default = try(docker_image.none[0].name, "fallback") +} + +// --- resource for_each driven by a parameter value -------------------------- +data "coder_parameter" "shards" { + name = "shards" + type = "list(string)" + default = jsonencode(["x", "y", "z"]) +} + +resource "docker_image" "shard" { + for_each = toset(jsondecode(data.coder_parameter.shards.value)) + name = "shard-${each.key}" +} + +// --- computed attribute: unknown with or without the resource --------------- +data "coder_parameter" "computed" { + name = "computed" + type = "string" + default = docker_image.base.image_id +} + +// --- two-level module chain -------------------------------------------------- +module "outer" { + source = "./outer" + image_name = docker_image.base.name +} + +data "coder_parameter" "nested" { + name = "nested" + type = "string" + default = module.outer.inner_name +} + +// --- multi-hop locals --------------------------------------------------------- +locals { + hop_c = docker_image.base.name + hop_b = local.hop_c + hop_a = local.hop_b +} + +data "coder_parameter" "hops" { + name = "hops" + type = "string" + default = local.hop_a +} + +// --- orphan: reachable from nothing ----------------------------------------- +resource "docker_container" "orphan" { + name = "orphan" + image = "does-not-exist" +} diff --git a/testdata/resourceclosure2/outer/inner/main.tf b/testdata/resourceclosure2/outer/inner/main.tf new file mode 100644 index 0000000..d67d15c --- /dev/null +++ b/testdata/resourceclosure2/outer/inner/main.tf @@ -0,0 +1,7 @@ +variable "image_name" { + type = string +} + +output "name" { + value = "inner-${var.image_name}" +} diff --git a/testdata/resourceclosure2/outer/main.tf b/testdata/resourceclosure2/outer/main.tf new file mode 100644 index 0000000..fb45653 --- /dev/null +++ b/testdata/resourceclosure2/outer/main.tf @@ -0,0 +1,12 @@ +variable "image_name" { + type = string +} + +module "inner" { + source = "./inner" + image_name = "outer-${var.image_name}" +} + +output "inner_name" { + value = module.inner.name +} diff --git a/testdata/resourceclosure2/skipe2e b/testdata/resourceclosure2/skipe2e new file mode 100644 index 0000000..36fb12c --- /dev/null +++ b/testdata/resourceclosure2/skipe2e @@ -0,0 +1 @@ +resource reachability is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here diff --git a/testdata/resourceclosuresubmod/main.tf b/testdata/resourceclosuresubmod/main.tf new file mode 100644 index 0000000..a82c629 --- /dev/null +++ b/testdata/resourceclosuresubmod/main.tf @@ -0,0 +1,30 @@ +// The only parameter lives in a submodule and reads a root resource through a +// module input. The root module has no parameter, preset, or tag block of its +// own, so a strategy that prunes root resources based on root targets has no +// basis to prune here and must keep everything. +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + } +} + +resource "docker_image" "base" { + name = "large" +} + +module "sub" { + source = "./sub" + image_name = docker_image.base.name +} + +resource "docker_container" "orphan" { + name = "orphan" + image = "does-not-exist" +} diff --git a/testdata/resourceclosuresubmod/skipe2e b/testdata/resourceclosuresubmod/skipe2e new file mode 100644 index 0000000..36fb12c --- /dev/null +++ b/testdata/resourceclosuresubmod/skipe2e @@ -0,0 +1 @@ +resource reachability is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here diff --git a/testdata/resourceclosuresubmod/sub/main.tf b/testdata/resourceclosuresubmod/sub/main.tf new file mode 100644 index 0000000..b029012 --- /dev/null +++ b/testdata/resourceclosuresubmod/sub/main.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + } +} + +variable "image_name" { + type = string +} + +data "coder_parameter" "flavor" { + name = "flavor" + type = "string" + default = var.image_name +}