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 19, 2024
1 parent c2663f8 commit 4c31b00
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 1 deletion.
13 changes: 13 additions & 0 deletions internal/stacks/stackruntime/internal/stackeval/for_each.go
Expand Up @@ -69,7 +69,20 @@ func evaluateForEachExpr(ctx context.Context, expr hcl.Expression, phase EvalPha
case ty.IsObjectType() || ty.IsMapType():
// okay

// TODO: Should we just remove this case? It seems like we can do some validation also on unknown values
case !result.Value.IsKnown():
// We can validate if the type is either a map, object or a set of strings
if !(ty.IsMapType() || ty.IsObjectType() || ty.IsSetType()) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: invalidForEachSummary,
Detail: invalidForEachDetail,
Subject: result.Expression.Range().Ptr(),
Expression: result.Expression,
EvalContext: result.EvalContext,
})
}

// we can't validate further without knowing the value
return result, diags

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 4c31b00

Please sign in to comment.