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
126 changes: 126 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,132 @@ 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"),
},
},
{
// file() resolves relative to the template root. A path inside the
// template is read; a path that escapes it is unknown, which
// invalidates that option but still renders the parameter.
name: "diskaccess",
dir: "diskaccess",
expTags: map[string]string{},
unknownTags: []string{},
params: map[string]assertParam{
"file": apWithDiags().
value("hello world").def("hello world").
optVals("hello world", types.UnknownStringValue).
errorDiagnostics("Parameter contains 1 invalid options"),
},
},
{
// First hop of the chain: ide_selector is count-gated on git_repo,
// which has no value yet, so only git_repo renders.
name: "chain-no-inputs",
dir: "chain",
input: preview.Input{
ParameterValues: map[string]string{},
},
expTags: map[string]string{},
unknownTags: []string{},
params: map[string]assertParam{
"git_repo": apWithDiags().errorDiagnostics("Required"),
},
},
{
// Second hop of the chain. cpu_cores is count-gated on
// data.coder_parameter.ide_selector[0].value, and ide_selector is
// itself count-gated. With ide_selector supplied, local.selected
// converges to ["GoLand"] (visible in Output.ModuleOutput), but
// cpu_cores' count is evaluated before that input has flowed in
// and count expansion is not revisited, so the parameter is
// silently dropped. This pins that limitation.
//
// If chained count gating is fixed, add:
//
// "cpu_cores": ap().value("4"),
//
// (a number-typed parameter) and the parameter count assertion
// will require all three.
name: "chain-inputs",
dir: "chain",
input: preview.Input{
ParameterValues: map[string]string{
"git_repo": "coder/coder",
"ide_selector": `["GoLand"]`,
"cpu_cores": "4",
},
},
expTags: map[string]string{},
unknownTags: []string{},
params: map[string]assertParam{
"git_repo": ap().value("coder/coder"),
"ide_selector": ap().value(`["GoLand"]`),
},
},
{
name: "sometags",
dir: "sometags",
Expand Down
99 changes: 99 additions & 0 deletions testdata/chain/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.5.3"
}
}
}

// Two-hop count chaining: ide_selector exists only once git_repo has a value,
// and cpu_cores exists only once ide_selector (itself count-gated, so reached
// via [0]) has a non-empty selection. See the "chain" vectors in Test_Extract
// for the current limitation on the second hop.
locals {
ides = [
"VS Code",
"JetBrains IntelliJ",
"GoLand",
"WebStorm",
"PyCharm",
"Databricks",
"Jupyter Notebook",
]

is_ml_repo = data.coder_parameter.git_repo.value == "coder/mlkit"

selected = try(jsondecode(data.coder_parameter.ide_selector[0].value), [])
}


data "coder_parameter" "git_repo" {
name = "git_repo"
display_name = "Git repo"
description = "Select a git repo to work on."
order = 1
mutable = true
type = "string"
form_type = "dropdown"

option {
# A Go-heavy repository
name = "coder/coder"
value = "coder/coder"
}

option {
# A python-heavy repository
name = "coder/mlkit"
value = "coder/mlkit"
}
}

data "coder_parameter" "ide_selector" {
count = try(data.coder_parameter.git_repo.value, "") != "" ? 1 : 0
name = "ide_selector"
description = "Choose any IDEs for your workspace."
mutable = true
display_name = "Select mutliple IDEs"
order = 1
default = "[]"

# Allows users to select multiple IDEs from the list.
form_type = "multi-select"
type = "list(string)"


dynamic "option" {
for_each = local.ides
content {
name = option.value
value = option.value
}
}
}


data "coder_parameter" "cpu_cores" {
# Only show this parameter if the previous box is selected.
count = length(local.selected) > 0 ? 1 : 0

name = "cpu_cores"
display_name = "CPU Cores"
type = "number"
form_type = "slider"
default = local.is_ml_repo ? 12 : 6
order = 2
validation {
min = 1
max = local.is_ml_repo ? 16 : 8
}
}

output "selected" {
value = local.selected
}

output "static" {
value = "foo"
}
1 change: 1 addition & 0 deletions testdata/chain/skipe2e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
chained count gating is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here
1 change: 1 addition & 0 deletions testdata/diskaccess/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
27 changes: 27 additions & 0 deletions testdata/diskaccess/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.4.0-pre0"
}
}
}


data "coder_parameter" "file" {
name = "file"
description = "Attempt to read some files."
type = "string"
order = 1
default = file("./hello.txt")

option {
name = "Local"
value = file("./hello.txt")
}

option {
name = "Outer"
value = file("../README.md")
}
}
1 change: 1 addition & 0 deletions testdata/diskaccess/skipe2e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file() reads are exercised by Test_Extract (static preview eval); the outer path is intentionally unreadable
Loading
Loading