stacks: apply nested default values to inputs (#35349)

* stacks: apply nested default values to inputs

* group similar tests
pull/35354/head
Liam Cervante 2 years ago committed by GitHub
parent 284ce63947
commit b646dff26a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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"
@ -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.
@ -105,13 +99,45 @@ 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,
@ -119,10 +145,11 @@ func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, t
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,
@ -130,50 +157,33 @@ func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, t
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
}

@ -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
@ -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
}

@ -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"
@ -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 {
@ -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 {
@ -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)

@ -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,
@ -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",
@ -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,

@ -375,6 +375,284 @@ func TestPlanWithVariableDefaults(t *testing.T) {
}
}
func TestPlanWithComplexVariableDefaults(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("complex-inputs"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(), nil
},
},
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
stackaddrs.InputVariable{Name: "optional"}: {
Value: cty.EmptyObjectVal, // This should be populated by defaults.
DefRange: tfdiags.SourceRange{},
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
changes, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Fatalf("unexpected diagnostics: %s", diags)
}
sort.SliceStable(changes, func(i, j int) bool {
return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanComplete: true,
PlanApplyable: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedInputValues: map[string]plans.DynamicValue{
"input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.NullVal(cty.String),
"value": cty.StringVal("hello, earth!"),
}),
})),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[0]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[0]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[1]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[1]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[2]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[2]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, earth!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("stack.child.component.parent"),
PlanComplete: true,
PlanApplyable: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedInputValues: map[string]plans.DynamicValue{
"input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.NullVal(cty.String),
"value": cty.StringVal("hello, earth!"),
}),
})),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[0]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[0]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[1]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[1]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[2]"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data[2]"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, earth!"),
})),
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "default"},
Value: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "optional"},
Value: cty.ObjectVal(map[string]cty.Value{
"id": cty.NullVal(cty.String),
"value": cty.StringVal("hello, earth!"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "optional_default"},
Value: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
}),
},
}
if diff := cmp.Diff(wantChanges, changes, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithSingleResource(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-single-resource")
@ -565,7 +843,7 @@ func TestPlanWithEphemeralInputVariables(t *testing.T) {
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
Applyable: false,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
@ -615,7 +893,7 @@ func TestPlanWithEphemeralInputVariables(t *testing.T) {
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
Applyable: false,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,

@ -0,0 +1,50 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
provider "testing" "default" {}
variable "default" {
type = object({
id = string
value = string
})
default = {
id = "cec9bc39"
value = "hello, mercury!"
}
}
variable "optional_default" {
type = object({
id = optional(string)
value = optional(string, "hello, venus!")
})
default = {
id = "78d8b3d7"
}
}
variable "optional" {
type = object({
id = optional(string)
value = optional(string, "hello, earth!")
})
}
component "parent" {
source = "../"
providers = {
testing = provider.testing.default
}
inputs = {
input = [
var.default,
var.optional_default,
var.optional,
]
}
}

@ -0,0 +1,21 @@
terraform {
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
}
variable "input" {
type = list(object({
id = string
value = string
}))
}
resource "testing_resource" "data" {
count = length(var.input)
id = var.input[count.index].id
value = var.input[count.index].value
}

@ -0,0 +1,58 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
provider "testing" "default" {}
variable "default" {
type = object({
id = string
value = string
})
default = {
id = "cec9bc39"
value = "hello, mercury!"
}
}
variable "optional_default" {
type = object({
id = optional(string)
value = optional(string, "hello, venus!")
})
default = {
id = "78d8b3d7"
}
}
variable "optional" {
type = object({
id = optional(string)
value = optional(string, "hello, earth!")
})
}
component "self" {
source = "./"
providers = {
testing = provider.testing.default
}
inputs = {
input = [
var.default,
var.optional_default,
var.optional,
]
}
}
stack "child" {
source = "./child"
inputs = {
optional = {}
}
}
Loading…
Cancel
Save