Skip to content

Commit

Permalink
stacks: validate unknown component for_each type
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt committed Apr 25, 2024
1 parent 2f07fba commit 9daebbe
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 10 deletions.
14 changes: 5 additions & 9 deletions internal/stacks/stackruntime/internal/stackeval/for_each.go
Expand Up @@ -69,22 +69,18 @@ func evaluateForEachExpr(ctx context.Context, expr hcl.Expression, phase EvalPha
case ty.IsObjectType() || ty.IsMapType():
// okay

case !result.Value.IsKnown():
// we can't validate further without knowing the value
return result, diags

case ty.IsSetType():
if markSafeLengthInt(result.Value) == 0 {
// we are okay with an empty set
return result, diags
}

// since we can't use a set values that are unknown, we treat the
// entire set as unknown
if !result.Value.IsWhollyKnown() {
return result, diags
}

if markSafeLengthInt(result.Value) == 0 {
// we are okay with an empty set
return result, diags
}

if !ty.ElementType().Equals(cty.String) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Expand Down
54 changes: 53 additions & 1 deletion internal/stacks/stackruntime/plan_test.go
Expand Up @@ -1816,7 +1816,59 @@ func TestPlanWithDeferredComponentForEachDueToParentComponentOutput(t *testing.T
}
}

// TODO: Test that we throw diagnostics if the output used in component for each has a non-set type
func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "deferred-component-for-each-from-component-of-invalid-type")

fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}

changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(), nil
},
},

ForcePlanTimestamp: &fakePlanTimestamp,

InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "components"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)

if len(diags) != 1 {
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
}

if diags[0].Severity() != tfdiags.Error {
t.Errorf("expected error diagnostic, got %q", diags[0].Severity())
}

expectedSummary := "Invalid for_each value"
if diags[0].Description().Summary != expectedSummary {
t.Errorf("expected diagnostic with summary %q, got %q", expectedSummary, diags[0].Description().Summary)
}

expectedDetail := "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component."
if diags[0].Description().Detail != expectedDetail {
t.Errorf("expected diagnostic with detail %q, got %q", expectedDetail, diags[0].Description().Detail)
}
}

// collectPlanOutput consumes the two output channels emitting results from
// a call to [Plan], and collects all of the data written to them before
Expand Down
@@ -0,0 +1,37 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}


provider "testing" "default" {}


component "parent" {
source = "./parent"

providers = {
testing = provider.testing.default
}

inputs = {
input = "parent"
}
}


component "self" {
source = "./self"

providers = {
testing = provider.testing.default
}

inputs = {
input = each.value
}

for_each = component.parent.letters_in_id // This is a list and no set or map
}
@@ -0,0 +1,27 @@
terraform {
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
}

variable "id" {
type = string
default = null
nullable = true # We'll generate an ID if none provided.
}

variable "input" {
type = string
}

resource "testing_resource" "data" {
id = var.id
value = var.input
}

output "letters_in_id" {
value = split("", testing_resource.data.id)
}
@@ -0,0 +1,23 @@
terraform {
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
}

variable "id" {
type = string
default = null
nullable = true # We'll generate an ID if none provided.
}

variable "input" {
type = string
}

resource "testing_resource" "data" {
id = var.id
value = var.input
}

0 comments on commit 9daebbe

Please sign in to comment.