Commit 7a6a7052 authored by Martin Atkins's avatar Martin Atkins
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.
No related merge requests found
Showing with 113 additions and 6 deletions
+113 -6
......@@ -6562,6 +6562,53 @@ func TestContext2Apply_outputBasic(t *testing.T) {
}
}
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)
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("Ermintrude"),
}),
"object_override": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Peppa"),
}),
}
ovs := state.RootModule().OutputValues
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)
}
}
}
func TestContext2Apply_outputAdd(t *testing.T) {
m1 := testModule(t, "apply-output-add-before")
p1 := testProvider("aws")
......
......@@ -6,6 +6,7 @@ import (
"github.com/hashicorp/hcl/v2"
"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"
......@@ -15,6 +16,7 @@ import (
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/hashicorp/terraform/internal/typeexpr"
)
// nodeExpandOutput is the placeholder for a non-root module output that has
......@@ -324,7 +326,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.Addr, n.Config.Expr, n.Config.ConstraintType, n.Config.TypeDefaults)
diags = diags.Append(evalDiags)
// We'll handle errors below, after we have loaded the module.
......@@ -389,6 +391,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, addr addrs.AbsOutputValue, 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{
......
......@@ -20,7 +20,7 @@ func TestNodeApplyableOutputExecute_knownValue(t *testing.T) {
ctx.RefreshStateState = 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{
......@@ -50,7 +50,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{
......@@ -78,6 +78,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}
......@@ -100,7 +101,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{
......@@ -123,8 +124,9 @@ func TestNodeApplyableOutputExecute_sensitiveValueAndOutput(t *testing.T) {
ctx.ChecksState = checks.NewState(nil)
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}
......
terraform {
experiments = [output_type_constraints]
}
output "string" {
type = string
value = true
}
output "object_default" {
type = object({
name = optional(string, "Ermintrude")
})
value = {}
}
output "object_override" {
type = object({
name = optional(string, "Ermintrude")
})
value = {
name = "Peppa"
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment