Skip to content

Commit

Permalink
stacks: apply nested default values to inputs (#35349)
Browse files Browse the repository at this point in the history
* stacks: apply nested default values to inputs

* group similar tests
  • Loading branch information
liamcervante committed Jun 18, 2024
1 parent 284ce63 commit b646dff
Show file tree
Hide file tree
Showing 8 changed files with 576 additions and 61 deletions.
110 changes: 60 additions & 50 deletions internal/stacks/stackaddrs/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -81,22 +82,15 @@ func ConfigComponentForAbsInstance(instAddr AbsComponentInstance) ConfigComponen
}

func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, tfdiags.Diagnostics) {
if traversal.IsRelative() {
// This is always a caller bug: caller must only pass absolute
// traversals in here.
panic("ParseAbsComponentInstance with relative traversal")
}

stackInst, remain, diags := parseInStackInstancePrefix(traversal)
inst, remain, diags := parseAbsComponentInstance(traversal)
if diags.HasErrors() {
return AbsComponentInstance{}, diags
}

// "remain" should now be the keyword "component" followed by a valid
// component name, optionally followed by an instance key, and nothing
// else.
const diagSummary = "Invalid component instance address"
if len(remain) < 2 || len(remain) > 3 {
if len(remain) > 0 {
// Then we have some remaining traversal steps that weren't consumed
// by the component instance address itself, which is an error when the
// caller is using this function.
rng := remain.SourceRange()
// if "remain" is empty then the source range would be zero length,
// and so we'll use the original traversal instead.
Expand All @@ -105,75 +99,91 @@ func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, t
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Summary: "Invalid component instance address",
Detail: "The component instance address must include the keyword \"component\" followed by a component name.",
Subject: &rng,
})
return AbsComponentInstance{}, diags
}

return inst, diags
}

func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos)
diags = diags.Append(hclDiags)
if diags.HasErrors() {
return AbsComponentInstance{}, diags
}

ret, moreDiags := ParseAbsComponentInstance(traversal)
diags = diags.Append(moreDiags)
return ret, diags
}

func parseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, hcl.Traversal, tfdiags.Diagnostics) {
if traversal.IsRelative() {
// This is always a caller bug: caller must only pass absolute
// traversals in here.
panic("parseAbsComponentInstance with relative traversal")
}

stackInst, remain, diags := parseInStackInstancePrefix(traversal)
if diags.HasErrors() {
return AbsComponentInstance{}, remain, diags
}

// "remain" should now be the keyword "component" followed by a valid
// component name, optionally followed by an instance key.
const diagSummary = "Invalid component instance address"

if kwStep, ok := remain[0].(hcl.TraverseAttr); !ok || kwStep.Name != "component" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Detail: "The component instance address must include the keyword \"component\" followed by a component name.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsComponentInstance{}, diags
return AbsComponentInstance{}, remain, diags
}
remain = remain[1:]

nameStep, ok := remain[1].(hcl.TraverseAttr)
nameStep, ok := remain[0].(hcl.TraverseAttr)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Detail: "The component instance address must include the keyword \"component\" followed by a component name.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsComponentInstance{}, diags
return AbsComponentInstance{}, remain, diags
}
remain = remain[1:]
componentAddr := ComponentInstance{
Component: Component{Name: nameStep.Name},
}

if len(remain) == 3 {
instStep, ok := remain[2].(hcl.TraverseIndex)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Detail: "The final part of a component instance address must be the instance key.",
Subject: remain[2].SourceRange().Ptr(),
})
}
var err error
componentAddr.Key, err = addrs.ParseInstanceKey(instStep.Key)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Detail: fmt.Sprintf("Invalid instance key: %s.", err),
Subject: instStep.SourceRange().Ptr(),
})
return AbsComponentInstance{}, diags
if len(remain) > 0 {
if instStep, ok := remain[0].(hcl.TraverseIndex); ok {
var err error
componentAddr.Key, err = addrs.ParseInstanceKey(instStep.Key)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: diagSummary,
Detail: fmt.Sprintf("Invalid instance key: %s.", err),
Subject: instStep.SourceRange().Ptr(),
})
return AbsComponentInstance{}, remain, diags
}

remain = remain[1:]
}
}

return AbsComponentInstance{
Stack: stackInst,
Item: componentAddr,
}, diags
}

func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos)
diags = diags.Append(hclDiags)
if diags.HasErrors() {
return AbsComponentInstance{}, diags
}

ret, moreDiags := ParseAbsComponentInstance(traversal)
diags = diags.Append(moreDiags)
return ret, diags
}, remain, diags
}
36 changes: 36 additions & 0 deletions internal/stacks/stackaddrs/in_component.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ package stackaddrs
import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// InConfigComponent represents addresses of objects that belong to the modules
Expand Down Expand Up @@ -122,3 +126,35 @@ type InComponentable interface {
addrs.UniqueKeyer
fmt.Stringer
}

func ParseAbsResourceInstanceObject(traversal hcl.Traversal) (AbsResourceInstanceObject, tfdiags.Diagnostics) {
stack, remain, diags := parseAbsComponentInstance(traversal)
if diags.HasErrors() {
return AbsResourceInstanceObject{}, diags
}

resource, diags := addrs.ParseAbsResourceInstance(remain)
if diags.HasErrors() {
return AbsResourceInstanceObject{}, diags
}

return AbsResourceInstanceObject{
Component: stack,
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: resource,
},
}, diags
}

func ParseAbsResourceInstanceObjectStr(s string) (AbsResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos)
diags = diags.Append(hclDiags)
if diags.HasErrors() {
return AbsResourceInstanceObject{}, diags
}

ret, moreDiags := ParseAbsResourceInstanceObject(traversal)
diags = diags.Append(moreDiags)
return ret, diags
}
35 changes: 35 additions & 0 deletions internal/stacks/stackruntime/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"github.com/hashicorp/go-slug/sourcebundle"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
Expand Down Expand Up @@ -206,6 +208,33 @@ func plannedChangeSortKey(change stackplan.PlannedChange) string {
}
}

func mustAbsResourceInstance(addr string) addrs.AbsResourceInstance {
ret, diags := addrs.ParseAbsResourceInstanceStr(addr)
if len(diags) > 0 {
panic(fmt.Sprintf("failed to parse resource instance address %q: %s", addr, diags))
}
return ret
}

func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject {
ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr)
if len(diags) > 0 {
panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags))
}
return ret
}

func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance {
ret, diags := stackaddrs.ParseAbsComponentInstanceStr(addr)
if len(diags) > 0 {
panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags))
}
return ret
}

// mustPlanDynamicValue is a helper function that constructs a
// plans.DynamicValue from the given cty.Value, panicking if the construction
// fails.
func mustPlanDynamicValue(v cty.Value) plans.DynamicValue {
ret, err := plans.NewDynamicValue(v, v.Type())
if err != nil {
Expand All @@ -214,6 +243,9 @@ func mustPlanDynamicValue(v cty.Value) plans.DynamicValue {
return ret
}

// mustPlanDynamicValueDynamicType is a helper function that constructs a
// plans.DynamicValue from the given cty.Value, using cty.DynamicPseudoType as
// the type, and panicking if the construction fails.
func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue {
ret, err := plans.NewDynamicValue(v, cty.DynamicPseudoType)
if err != nil {
Expand All @@ -222,6 +254,9 @@ func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue {
return ret
}

// mustPlanDynamicValueSchema is a helper function that constructs a
// plans.DynamicValue from the given cty.Value and configschema.Block, panicking
// if the construction fails.
func mustPlanDynamicValueSchema(v cty.Value, block *configschema.Block) plans.DynamicValue {
ty := block.ImpliedType()
ret, err := plans.NewDynamicValue(v, ty)
Expand Down
45 changes: 36 additions & 9 deletions internal/stacks/stackruntime/internal/stackeval/input_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Va

switch {
case v.Addr().Stack.IsRoot():
wantTy := decl.Type.Constraint
var err error

wantTy := decl.Type.Constraint
extVal := v.main.RootVariableValue(ctx, v.Addr().Item, phase)

// We treat a null value as equivalent to an unspecified value,
Expand All @@ -125,8 +126,8 @@ func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Va
if extVal.Value.IsNull() {
// A separate code path will validate the default value, so
// we don't need to do that here.
defVal := cfg.DefaultValue(ctx)
if defVal == cty.NilVal {
val := cfg.DefaultValue(ctx)
if val == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Expand All @@ -136,18 +137,44 @@ func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Va
return cty.UnknownVal(wantTy), diags
}

extVal = ExternalInputValue{
Value: defVal,
DefRange: cfg.Declaration().DeclRange,
// The DefaultValue method already validated the default
// value, and applied the defaults, so we don't need to
// do that again.

val, err = convert.Convert(val, wantTy)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid value for root input variable",
Detail: fmt.Sprintf(
"Cannot use the given value for input variable %q: %s.",
v.Addr().Item.Name, err,
),
})
val = cfg.markValue(cty.UnknownVal(wantTy))
return val, diags
}

// TODO: check the value against any custom validation rules
// declared in the configuration.
return cfg.markValue(val), diags
}

// Otherwise, we'll use the provided value.
val := extVal.Value

// First, apply any defaults that are declared in the
// configuration.
if defaults := decl.Type.Defaults; defaults != nil {
val = defaults.Apply(val)
}

val, err := convert.Convert(extVal.Value, wantTy)
const errSummary = "Invalid value for root input variable"
// Next, convert the value to the expected type.
val, err = convert.Convert(val, wantTy)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: errSummary,
Summary: "Invalid value for root input variable",
Detail: fmt.Sprintf(
"Cannot use the given value for input variable %q: %s.",
v.Addr().Item.Name, err,
Expand Down
Loading

0 comments on commit b646dff

Please sign in to comment.