Skip to content

Commit 822b02c

Browse files
core: Apply default values and type constraints to output values
The ability to set actually set a type constraint on an output value is currently guarded by the "output_type_constraints" experiment opt-in, so any non-experiment-using configuration will always have ConstraintType set to cty.DynamicPseudoType as shown in some of the unit tests updated here. Those who opt in to the experiment can specify a type constraint and defaults using exactly the same syntax as for input variables, and essentially get the same effect but in reverse: the type constraint and defaults get applied at the boundary where the value is leaving the module where it's declared, as opposed to when it's entering into a module as for input variables.
1 parent c649254 commit 822b02c

File tree

4 files changed

+113
-6
lines changed

4 files changed

+113
-6
lines changed

internal/terraform/context_apply_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6562,6 +6562,53 @@ func TestContext2Apply_outputBasic(t *testing.T) {
65626562
}
65636563
}
65646564

6565+
func TestContext2Apply_outputWithTypeContraint(t *testing.T) {
6566+
m := testModule(t, "apply-output-type-constraint")
6567+
p := testProvider("aws")
6568+
p.PlanResourceChangeFn = testDiffFn
6569+
p.ApplyResourceChangeFn = testApplyFn
6570+
ctx := testContext2(t, &ContextOpts{
6571+
Providers: map[addrs.Provider]providers.Factory{
6572+
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
6573+
},
6574+
})
6575+
6576+
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
6577+
assertNoErrors(t, diags)
6578+
6579+
state, diags := ctx.Apply(plan, m)
6580+
if diags.HasErrors() {
6581+
t.Fatalf("diags: %s", diags.Err())
6582+
}
6583+
6584+
wantValues := map[string]cty.Value{
6585+
"string": cty.StringVal("true"),
6586+
"object_default": cty.ObjectVal(map[string]cty.Value{
6587+
"name": cty.StringVal("Ermintrude"),
6588+
}),
6589+
"object_override": cty.ObjectVal(map[string]cty.Value{
6590+
"name": cty.StringVal("Peppa"),
6591+
}),
6592+
}
6593+
ovs := state.RootModule().OutputValues
6594+
for name, want := range wantValues {
6595+
os, ok := ovs[name]
6596+
if !ok {
6597+
t.Errorf("missing output value %q", name)
6598+
continue
6599+
}
6600+
if got := os.Value; !want.RawEquals(got) {
6601+
t.Errorf("wrong value for output %q\ngot: %#v\nwant: %#v", name, got, want)
6602+
}
6603+
}
6604+
6605+
for gotName := range ovs {
6606+
if _, ok := wantValues[gotName]; !ok {
6607+
t.Errorf("unexpected extra output value %q", gotName)
6608+
}
6609+
}
6610+
}
6611+
65656612
func TestContext2Apply_outputAdd(t *testing.T) {
65666613
m1 := testModule(t, "apply-output-add-before")
65676614
p1 := testProvider("aws")

internal/terraform/node_output.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/hashicorp/hcl/v2"
88
"github.com/zclconf/go-cty/cty"
9+
"github.com/zclconf/go-cty/cty/convert"
910

1011
"github.com/hashicorp/terraform/internal/addrs"
1112
"github.com/hashicorp/terraform/internal/configs"
@@ -15,6 +16,7 @@ import (
1516
"github.com/hashicorp/terraform/internal/plans"
1617
"github.com/hashicorp/terraform/internal/states"
1718
"github.com/hashicorp/terraform/internal/tfdiags"
19+
"github.com/hashicorp/terraform/internal/typeexpr"
1820
)
1921

2022
// nodeExpandOutput is the placeholder for a non-root module output that has
@@ -341,7 +343,7 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
341343
// This has to run before we have a state lock, since evaluation also
342344
// reads the state
343345
var evalDiags tfdiags.Diagnostics
344-
val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
346+
val, evalDiags = evalOutputValue(ctx, n.Addr, n.Config.Expr, n.Config.ConstraintType, n.Config.TypeDefaults)
345347
diags = diags.Append(evalDiags)
346348

347349
// We'll handle errors below, after we have loaded the module.
@@ -406,6 +408,38 @@ If you do intend to export this data, annotate the output value as sensitive by
406408
return diags
407409
}
408410

411+
// evalOutputValue encapsulates the logic for transforming an author's value
412+
// expression into a valid value of their declared type constraint, or returning
413+
// an error describing why that isn't possible.
414+
func evalOutputValue(ctx EvalContext, addr addrs.AbsOutputValue, expr hcl.Expression, wantType cty.Type, defaults *typeexpr.Defaults) (cty.Value, tfdiags.Diagnostics) {
415+
// We can't pass wantType to EvaluateExpr here because we'll need to
416+
// possibly apply our defaults before attempting type conversion below.
417+
val, diags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
418+
if diags.HasErrors() {
419+
return cty.UnknownVal(wantType), diags
420+
}
421+
422+
if defaults != nil {
423+
val = defaults.Apply(val)
424+
}
425+
426+
val, err := convert.Convert(val, wantType)
427+
if err != nil {
428+
diags = diags.Append(&hcl.Diagnostic{
429+
Severity: hcl.DiagError,
430+
Summary: "Invalid output value",
431+
Detail: fmt.Sprintf("The value expression does not match this output value's type constraint: %s.", tfdiags.FormatError(err)),
432+
Subject: expr.Range().Ptr(),
433+
// TODO: Populate EvalContext and Expression, but we can't do that
434+
// as long as we're using the ctx.EvaluateExpr helper above because
435+
// the EvalContext is hidden from us in that case.
436+
})
437+
return cty.UnknownVal(wantType), diags
438+
}
439+
440+
return val, diags
441+
}
442+
409443
// dag.GraphNodeDotter impl.
410444
func (n *NodeApplyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
411445
return &dag.DotNode{

internal/terraform/node_output_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestNodeApplyableOutputExecute_knownValue(t *testing.T) {
2020
ctx.RefreshStateState = states.NewState().SyncWrapper()
2121
ctx.ChecksState = checks.NewState(nil)
2222

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

53-
config := &configs.Output{Name: "map-output"}
53+
config := &configs.Output{Name: "map-output", ConstraintType: cty.DynamicPseudoType}
5454
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
5555
node := &NodeApplyableOutput{Config: config, Addr: addr}
5656
val := cty.MapVal(map[string]cty.Value{
@@ -78,6 +78,7 @@ func TestNodeApplyableOutputExecute_invalidDependsOn(t *testing.T) {
7878
hcl.TraverseAttr{Name: "bar"},
7979
},
8080
},
81+
ConstraintType: cty.DynamicPseudoType,
8182
}
8283
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
8384
node := &NodeApplyableOutput{Config: config, Addr: addr}
@@ -100,7 +101,7 @@ func TestNodeApplyableOutputExecute_sensitiveValueNotOutput(t *testing.T) {
100101
ctx.StateState = states.NewState().SyncWrapper()
101102
ctx.ChecksState = checks.NewState(nil)
102103

103-
config := &configs.Output{Name: "map-output"}
104+
config := &configs.Output{Name: "map-output", ConstraintType: cty.DynamicPseudoType}
104105
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
105106
node := &NodeApplyableOutput{Config: config, Addr: addr}
106107
val := cty.MapVal(map[string]cty.Value{
@@ -123,8 +124,9 @@ func TestNodeApplyableOutputExecute_sensitiveValueAndOutput(t *testing.T) {
123124
ctx.ChecksState = checks.NewState(nil)
124125

125126
config := &configs.Output{
126-
Name: "map-output",
127-
Sensitive: true,
127+
Name: "map-output",
128+
Sensitive: true,
129+
ConstraintType: cty.DynamicPseudoType,
128130
}
129131
addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance)
130132
node := &NodeApplyableOutput{Config: config, Addr: addr}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
terraform {
2+
experiments = [output_type_constraints]
3+
}
4+
5+
output "string" {
6+
type = string
7+
value = true
8+
}
9+
10+
output "object_default" {
11+
type = object({
12+
name = optional(string, "Ermintrude")
13+
})
14+
value = {}
15+
}
16+
17+
output "object_override" {
18+
type = object({
19+
name = optional(string, "Ermintrude")
20+
})
21+
value = {
22+
name = "Peppa"
23+
}
24+
}

0 commit comments

Comments
 (0)