diff --git a/internal/stacks/stackaddrs/component.go b/internal/stacks/stackaddrs/component.go index 973809e7f0..efeabb25ac 100644 --- a/internal/stacks/stackaddrs/component.go +++ b/internal/stacks/stackaddrs/component.go @@ -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 } diff --git a/internal/stacks/stackaddrs/in_component.go b/internal/stacks/stackaddrs/in_component.go index c4b8d17e78..1db9951c43 100644 --- a/internal/stacks/stackaddrs/in_component.go +++ b/internal/stacks/stackaddrs/in_component.go @@ -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 +} diff --git a/internal/stacks/stackruntime/helper_test.go b/internal/stacks/stackruntime/helper_test.go index 05840694ab..a9fa8ac143 100644 --- a/internal/stacks/stackruntime/helper_test.go +++ b/internal/stacks/stackruntime/helper_test.go @@ -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) diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go index b1abe2b9c2..f61cc3619a 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -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, diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 411b54f0b9..797e28b0d0 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -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, diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl new file mode 100644 index 0000000000..06b3249093 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl @@ -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, + ] + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf new file mode 100644 index 0000000000..20aeb58ddf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf @@ -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 +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl new file mode 100644 index 0000000000..629cf6170c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl @@ -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 = {} + } +}