Skip to content
Merged
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
62 changes: 62 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
134 changes: 134 additions & 0 deletions testdata/resourceclosure2/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions testdata/resourceclosure2/outer/inner/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "image_name" {
type = string
}

output "name" {
value = "inner-${var.image_name}"
}
12 changes: 12 additions & 0 deletions testdata/resourceclosure2/outer/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions testdata/resourceclosure2/skipe2e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
resource reachability is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here
30 changes: 30 additions & 0 deletions testdata/resourceclosuresubmod/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions testdata/resourceclosuresubmod/skipe2e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
resource reachability is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here
18 changes: 18 additions & 0 deletions testdata/resourceclosuresubmod/sub/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
Loading