mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 08:58:34 -04:00
stacks: apply nested default values to inputs (#35349)
* stacks: apply nested default values to inputs * group similar tests
This commit is contained in:
parent
284ce63947
commit
b646dff26a
8 changed files with 590 additions and 75 deletions
|
|
@ -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,64 +99,14 @@ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
nameStep, ok := remain[1].(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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return AbsComponentInstance{
|
||||
Stack: stackInst,
|
||||
Item: componentAddr,
|
||||
}, diags
|
||||
return inst, diags
|
||||
}
|
||||
|
||||
func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) {
|
||||
|
|
@ -177,3 +121,69 @@ func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagn
|
|||
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{}, remain, diags
|
||||
}
|
||||
remain = remain[1:]
|
||||
|
||||
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{}, remain, diags
|
||||
}
|
||||
remain = remain[1:]
|
||||
componentAddr := ComponentInstance{
|
||||
Component: Component{Name: nameStep.Name},
|
||||
}
|
||||
|
||||
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,
|
||||
}, 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
|
||||
}
|
||||
|
||||
val, err := convert.Convert(extVal.Value, wantTy)
|
||||
const errSummary = "Invalid value for root input variable"
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
|
|||
50
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl
vendored
Normal file
50
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl
vendored
Normal file
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
21
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf
vendored
Normal file
21
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
58
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl
vendored
Normal file
58
internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl
vendored
Normal file
|
|
@ -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…
Reference in a new issue