Skip to content

Commit

Permalink
Check type contraints for output values
Browse files Browse the repository at this point in the history
  • Loading branch information
dbanck committed Jan 24, 2025
1 parent 3b7d884 commit 2487e2a
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 9 deletions.
53 changes: 50 additions & 3 deletions internal/terraform/context_apply2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2830,7 +2830,7 @@ func TestContext2Apply_destroy_and_forget(t *testing.T) {
resource "test_object" "a" {
test_string = "foo"
}
resource "test_object" "b" {
test_string = "foo"
}
Expand Down Expand Up @@ -2876,7 +2876,7 @@ func TestContext2Apply_destroy_and_forget(t *testing.T) {
}
resource "test_object" "a" {
for_each = local.items
test_string = each.value
}
`,
Expand Down Expand Up @@ -2982,7 +2982,7 @@ func TestContext2Apply_destroy_and_forget_single_resource(t *testing.T) {
"main.tf": `
removed {
from = test_object.a
lifecycle {
destroy = false
}
Expand Down Expand Up @@ -3766,3 +3766,50 @@ resource "test_object" "c" {
}
t.Fatal("failed to find destroy destroy dependency between test_object.a(destroy) and test_object.c(destroy)")
}

func TestContext2Apply_outputWithTypeContraint(t *testing.T) {
m := testModule(t, "apply-output-type-constraint")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})

plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)

state, diags := ctx.Apply(plan, m, nil)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}

wantValues := map[string]cty.Value{
"string": cty.StringVal("true"),
"object_default": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Bart"),
}),
"object_override": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Lisa"),
}),
}
ovs := state.RootOutputValues
for name, want := range wantValues {
os, ok := ovs[name]
if !ok {
t.Errorf("missing output value %q", name)
continue
}
if got := os.Value; !want.RawEquals(got) {
t.Errorf("wrong value for output %q\ngot: %#v\nwant: %#v", name, got, want)
}
}

for gotName := range ovs {
if _, ok := wantValues[gotName]; !ok {
t.Errorf("unexpected extra output value %q", gotName)
}
}
}
36 changes: 35 additions & 1 deletion internal/terraform/node_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"log"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
Expand Down Expand Up @@ -436,7 +438,7 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
// This has to run before we have a state lock, since evaluation also
// reads the state
var evalDiags tfdiags.Diagnostics
val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
val, evalDiags = evalOutputValue(ctx, n.Config.Expr, n.Config.ConstraintType, n.Config.TypeDefaults)
diags = diags.Append(evalDiags)

// We'll handle errors below, after we have loaded the module.
Expand Down Expand Up @@ -529,6 +531,38 @@ If you do intend to export this data, annotate the output value as sensitive by
return diags
}

// evalOutputValue encapsulates the logic for transforming an author's value
// expression into a valid value of their declared type constraint, or returning
// an error describing why that isn't possible.
func evalOutputValue(ctx EvalContext, expr hcl.Expression, wantType cty.Type, defaults *typeexpr.Defaults) (cty.Value, tfdiags.Diagnostics) {
// We can't pass wantType to EvaluateExpr here because we'll need to
// possibly apply our defaults before attempting type conversion below.
val, diags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
if diags.HasErrors() {
return cty.UnknownVal(wantType), diags
}

if defaults != nil {
val = defaults.Apply(val)
}

val, err := convert.Convert(val, wantType)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid output value",
Detail: fmt.Sprintf("The value expression does not match this output value's type constraint: %s.", tfdiags.FormatError(err)),
Subject: expr.Range().Ptr(),
// TODO: Populate EvalContext and Expression, but we can't do that
// as long as we're using the ctx.EvaluateExpr helper above because
// the EvalContext is hidden from us in that case.
})
return cty.UnknownVal(wantType), diags
}

return val, diags
}

// dag.GraphNodeDotter impl.
func (n *NodeApplyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Expand Down
12 changes: 7 additions & 5 deletions internal/terraform/node_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestNodeApplyableOutputExecute_knownValue(t *testing.T) {
ctx.ChecksState = checks.NewState(nil)
ctx.DeferralsState = deferring.NewDeferred(false)

config := &configs.Output{Name: "map-output"}
config := &configs.Output{Name: "map-output", ConstraintType: cty.DynamicPseudoType}
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
node := &NodeApplyableOutput{Config: config, Addr: addr}
val := cty.MapVal(map[string]cty.Value{
Expand Down Expand Up @@ -55,7 +55,7 @@ func TestNodeApplyableOutputExecute_knownValue(t *testing.T) {
func TestNodeApplyableOutputExecute_noState(t *testing.T) {
ctx := new(MockEvalContext)

config := &configs.Output{Name: "map-output"}
config := &configs.Output{Name: "map-output", ConstraintType: cty.DynamicPseudoType}
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
node := &NodeApplyableOutput{Config: config, Addr: addr}
val := cty.MapVal(map[string]cty.Value{
Expand Down Expand Up @@ -83,6 +83,7 @@ func TestNodeApplyableOutputExecute_invalidDependsOn(t *testing.T) {
hcl.TraverseAttr{Name: "bar"},
},
},
ConstraintType: cty.DynamicPseudoType,
}
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
node := &NodeApplyableOutput{Config: config, Addr: addr}
Expand All @@ -105,7 +106,7 @@ func TestNodeApplyableOutputExecute_sensitiveValueNotOutput(t *testing.T) {
ctx.StateState = states.NewState().SyncWrapper()
ctx.ChecksState = checks.NewState(nil)

config := &configs.Output{Name: "map-output"}
config := &configs.Output{Name: "map-output", ConstraintType: cty.DynamicPseudoType}
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
node := &NodeApplyableOutput{Config: config, Addr: addr}
val := cty.MapVal(map[string]cty.Value{
Expand All @@ -129,8 +130,9 @@ func TestNodeApplyableOutputExecute_sensitiveValueAndOutput(t *testing.T) {
ctx.DeferralsState = deferring.NewDeferred(false)

config := &configs.Output{
Name: "map-output",
Sensitive: true,
Name: "map-output",
Sensitive: true,
ConstraintType: cty.DynamicPseudoType,
}
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
node := &NodeApplyableOutput{Config: config, Addr: addr}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
output "string" {
type = string
value = true
}

output "object_default" {
type = object({
name = optional(string, "Bart")
})
value = {}
}

output "object_override" {
type = object({
name = optional(string, "Bart")
})
value = {
name = "Lisa"
}
}

0 comments on commit 2487e2a

Please sign in to comment.