blob: a19b1678d3c5628d2e192b274bab1838fb84cc76 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"context"
"encoding/json"
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
"github.com/hashicorp/terraform/internal/addrs"
terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
default_testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval"
stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/hashicorp/terraform/version"
)
// TestPlan_valid runs the same set of configurations as TestValidate_valid.
//
// Plan should execute the same set of validations as validate, so we expect
// all of the following to be valid for both plan and validate.
//
// We also want to make sure the static and dynamic evaluations are not
// returning duplicate / conflicting diagnostics. This test will tell us if
// either plan or validate is reporting diagnostics the others are missing.
func TestPlan_valid(t *testing.T) {
for name, tc := range validConfigurations {
t.Run(name, func(t *testing.T) {
if tc.skip {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
lock.SetProvider(
addrs.NewDefaultProvider("other"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
// We also support an "other" provider out of the box to
// test the provider aliasing feature.
addrs.NewDefaultProvider("other"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
dependencyLocks: *lock,
timestamp: &fakePlanTimestamp,
}
cycle := TestCycle{
planInputs: tc.planInputVars,
wantPlannedChanges: nil, // don't care about the planned changes in this test.
wantPlannedDiags: nil, // should return no diagnostics.
}
testContext.Plan(t, ctx, nil, cycle)
})
}
}
// TestPlan_invalid runs the same set of configurations as TestValidate_invalid.
//
// Plan should execute the same set of validations as validate, so we expect
// all of the following to be invalid for both plan and validate.
//
// We also want to make sure the static and dynamic evaluations are not
// returning duplicate / conflicting diagnostics. This test will tell us if
// either plan or validate is reporting diagnostics the others are missing.
//
// The dynamic validation that happens during the plan *might* introduce
// additional diagnostics that are not present in the static validation. These
// should be added manually into this function.
func TestPlan_invalid(t *testing.T) {
for name, tc := range invalidConfigurations {
t.Run(name, func(t *testing.T) {
if tc.skip {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
dependencyLocks: *lock,
timestamp: &fakePlanTimestamp,
}
cycle := TestCycle{
planInputs: tc.planInputVars,
wantPlannedChanges: nil, // don't care about the planned changes in this test.
wantPlannedDiags: tc.diags(),
}
testContext.Plan(t, ctx, nil, cycle)
})
}
}
// TestPlan uses a generic framework for running plan integration tests
// against Stacks. Generally, new tests should be added into this function
// rather than copying the large amount of duplicate code from the other
// tests in this file.
//
// If you are editing other tests in this file, please consider moving them
// into this test function so they can reuse the shared setup and boilerplate
// code managing the boring parts of the test.
func TestPlan(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
tcs := map[string]struct {
path string
state *stackstate.State
store *stacks_testing_provider.ResourceStore
cycle TestCycle
}{
"empty-destroy-with-data-source": {
path: path.Join("with-data-source", "dependent"),
cycle: TestCycle{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"id": cty.StringVal("foo"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.data"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Delete,
Mode: plans.DestroyMode,
RequiredComponents: collections.NewSet(mustAbsComponent("component.self")),
PlannedOutputValues: make(map[string]cty.Value),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanComplete: true,
PlanApplyable: true,
Action: plans.Delete,
Mode: plans.DestroyMode,
PlannedOutputValues: map[string]cty.Value{
"id": cty.StringVal("foo"),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("id"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("foo"),
DeleteOnApply: true,
},
},
},
},
"deferred-provider-with-write-only": {
path: "with-write-only-attribute",
cycle: TestCycle{
planInputs: map[string]cty.Value{
"providers": cty.UnknownVal(cty.Set(cty.String)),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.main"),
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"datasource_id": mustPlanDynamicValueDynamicType(cty.StringVal("datasource")),
"resource_id": mustPlanDynamicValueDynamicType(cty.StringVal("resource")),
"write_only_input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"datasource_id": nil,
"resource_id": nil,
"write_only_input": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.data.testing_write_only_data_source.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("data.testing_write_only_data_source.data"),
PrevRunAddr: mustAbsResourceInstance("data.testing_write_only_data_source.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Read,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("datasource"),
"value": cty.UnknownVal(cty.String),
"write_only": cty.NullVal(cty.String),
})),
AfterSensitivePaths: []cty.Path{
cty.GetAttrPath("write_only"),
},
},
ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.WriteOnlyDataSourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.testing_write_only_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_write_only_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_write_only_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("resource"),
"value": cty.UnknownVal(cty.String),
"write_only": cty.NullVal(cty.String),
})),
AfterSensitivePaths: []cty.Path{
cty.GetAttrPath("write_only"),
},
},
},
PriorStateSrc: nil,
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.WriteOnlyResourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("providers"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
},
"deferred-provider-with-data-sources": {
path: path.Join("with-data-source", "deferred-provider-for-each"),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("data_known", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("data_known"),
"value": cty.StringVal("known"),
})).
Build(),
cycle: TestCycle{
planInputs: map[string]cty.Value{
"providers": cty.UnknownVal(cty.Set(cty.String)),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.const"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("data_known")),
"resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_known")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"resource": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"),
ChangeSrc: nil,
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "data_known",
"value": "known",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingDataSourceSchema,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("resource_known"),
"value": cty.StringVal("known"),
})),
},
},
PriorStateSrc: nil,
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.main[*]"),
PlanApplyable: false, // only deferred changes
PlanComplete: false, // deferred
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("data_unknown")),
"resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_unknown")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"resource": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "main",
},
Key: addrs.WildcardKey,
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: mustAbsResourceInstance("data.testing_data_source.data"),
},
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("data.testing_data_source.data"),
PrevRunAddr: mustAbsResourceInstance("data.testing_data_source.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Read,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("data_unknown"),
"value": cty.UnknownVal(cty.String),
})),
},
ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
},
PriorStateSrc: nil,
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingDataSourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "main",
},
Key: addrs.WildcardKey,
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: mustAbsResourceInstance("testing_resource.data"),
},
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("resource_unknown"),
"value": cty.UnknownVal(cty.String),
})),
},
},
PriorStateSrc: nil,
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("providers"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
store := tc.store
if store == nil {
store = stacks_testing_provider.NewResourceStore()
}
testContext := TestContext{
timestamp: &fakePlanTimestamp,
config: loadMainBundleConfigForTest(t, tc.path),
providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
},
dependencyLocks: *lock,
}
testContext.Plan(t, ctx, tc.state, tc.cycle)
})
}
}
func TestPlanWithMissingInputVariable(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "plan-undeclared-variable-in-component")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, gotDiags := collectPlanOutput(changesCh, diagsCh)
// We'll normalize the diagnostics to be of consistent underlying type
// using ForRPC, so that we can easily diff them; we don't actually care
// about which underlying implementation is in use.
gotDiags = gotDiags.ForRPC()
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared input variable",
Detail: `There is no variable "input" block declared in this stack.`,
Subject: &hcl.Range{
Filename: mainBundleSourceAddrStr("plan-undeclared-variable-in-component/undeclared-variable.tfstack.hcl"),
Start: hcl.Pos{Line: 17, Column: 13, Byte: 250},
End: hcl.Pos{Line: 17, Column: 22, Byte: 259},
},
})
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
}
func TestPlanWithNoValueForRequiredVariable(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "plan-no-value-for-required-variable")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, gotDiags := collectPlanOutput(changesCh, diagsCh)
// We'll normalize the diagnostics to be of consistent underlying type
// using ForRPC, so that we can easily diff them; we don't actually care
// about which underlying implementation is in use.
gotDiags = gotDiags.ForRPC()
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Detail: `The root input variable "var.beep" is not set, and has no default value.`,
Subject: &hcl.Range{
Filename: mainBundleSourceAddrStr("plan-no-value-for-required-variable/unset-variable.tfstack.hcl"),
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
})
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
}
func TestPlanWithVariableDefaults(t *testing.T) {
// Test that defaults are applied correctly for both unspecified input
// variables and those with an explicit null value.
testCases := map[string]struct {
inputs map[stackaddrs.InputVariable]ExternalInputValue
}{
"unspecified": {
inputs: make(map[stackaddrs.InputVariable]ExternalInputValue),
},
"explicit null": {
inputs: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "beep"}: {
Value: cty.NullVal(cty.DynamicPseudoType),
DefRange: tfdiags.SourceRange{Filename: "fake.tfstack.hcl"},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "plan-variable-defaults")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
InputValues: tc.inputs,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "beep"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("BEEP"),
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "defaulted"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("BOOP"),
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "specified"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("BEEP"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "beep",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("BEEP"),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
})
}
}
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)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{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.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&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"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("cec9bc39"),
"value": cty.StringVal("hello, mercury!"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "optional"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.NullVal(cty.String),
"value": cty.StringVal("hello, earth!"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "optional_default"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("78d8b3d7"),
"value": cty.StringVal("hello, venus!"),
}),
},
}
if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithSingleResource(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-single-resource")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
// The order of emission for our planned changes is unspecified since it
// depends on how the various goroutines get scheduled, and so we'll
// arbitrarily sort gotChanges lexically by the name of the change type
// so that we have some dependable order to diff against below.
sort.Slice(gotChanges, func(i, j int) bool {
ic := gotChanges[i]
jc := gotChanges[j]
return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc)
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"input": cty.StringVal("hello"),
"output": cty.UnknownVal(cty.String),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "obj"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"input": cty.StringVal("hello"),
"output": cty.UnknownVal(cty.String),
}),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewBuiltInProvider("terraform"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: plans.DynamicValue{
// This is an object conforming to the terraform_data
// resource type's schema.
//
// FIXME: Should write this a different way that is
// scrutable and won't break each time something gets
// added to the terraform_data schema. (We can't use
// mustPlanDynamicValue here because the resource type
// uses DynamicPseudoType attributes, which require
// explicitly-typed encoding.)
0x84, 0xa2, 0x69, 0x64, 0xc7, 0x03, 0x0c, 0x81,
0x01, 0xc2, 0xa5, 0x69, 0x6e, 0x70, 0x75, 0x74,
0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x22, 0xa5, 0x68, 0x65, 0x6c, 0x6c,
0x6f, 0xa6, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74,
0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x22, 0xd4, 0x00, 0x00, 0xb0, 0x74,
0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x5f,
0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0xc0,
},
},
},
// The following is schema for the real terraform_data resource
// type from the real terraform.io/builtin/terraform provider
// maintained elsewhere in this codebase. If that schema changes
// in future then this should change to match it.
Schema: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"input": {Type: cty.DynamicPseudoType, Optional: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
"triggers_replace": {Type: cty.DynamicPseudoType, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
Identity: &configschema.Object{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Description: "The unique identifier for the data store.",
Required: true,
},
},
Nesting: configschema.NestingSingle,
},
},
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithEphemeralInputVariables(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "variable-ephemeral")
t.Run("with variables set", func(t *testing.T) {
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
req := PlanRequest{
Config: cfg,
InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{
{Name: "eph"}: {Value: cty.StringVal("eph value")},
{Name: "noneph"}: {Value: cty.StringVal("noneph value")},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "eph",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.String), // ephemeral
RequiredOnApply: true,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "noneph",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("noneph value"),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
})
t.Run("without variables set", func(t *testing.T) {
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
req := PlanRequest{
InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{
// Intentionally not set for this subtest.
},
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "eph",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.String), // ephemeral
RequiredOnApply: false,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "noneph",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.String),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
})
}
func TestPlanVariableOutputRoundtripNested(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "variable-output-roundtrip-nested")
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "msg"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("default"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "msg",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("default"),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanSensitiveOutput(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "sensitive-output")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"out": cty.StringVal("secret").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "result"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("secret").Mark(marks.Sensitive),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanSensitiveOutputNested(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "sensitive-output-nested")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "result"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("secret").Mark(marks.Sensitive),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("child", addrs.NoKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"out": cty.StringVal("secret").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanSensitiveOutputAsInput(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "sensitive-output-as-input")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("stack.sensitive.component.self"),
),
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: map[string]plans.DynamicValue{
"secret": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"secret": {
{
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
},
PlannedOutputValues: map[string]cty.Value{
"result": cty.StringVal("SECRET").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "result"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType), // MessagePack nil
After: cty.StringVal("SECRET").Mark(marks.Sensitive),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"out": cty.StringVal("secret").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithProviderConfig(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-provider-config")
providerAddr := addrs.MustParseProviderSourceString("example.com/test/test")
providerSchema := &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
},
},
},
},
}
inputVarAddr := stackaddrs.InputVariable{Name: "name"}
fakeSrcRng := tfdiags.SourceRange{
Filename: "fake-source",
}
lock := depsfile.NewLocks()
lock.SetProvider(
providerAddr,
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
t.Run("valid", func(t *testing.T) {
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
provider := &default_testing_provider.MockProvider{
GetProviderSchemaResponse: providerSchema,
ValidateProviderConfigResponse: &providers.ValidateProviderConfigResponse{},
ConfigureProviderResponse: &providers.ConfigureProviderResponse{},
}
req := PlanRequest{
Config: cfg,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
inputVarAddr: {
Value: cty.StringVal("Jackson"),
DefRange: fakeSrcRng,
},
},
ProviderFactories: map[addrs.Provider]providers.Factory{
providerAddr: func() (providers.Interface, error) {
return provider, nil
},
},
DependencyLocks: *lock,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
if !provider.ValidateProviderConfigCalled {
t.Error("ValidateProviderConfig wasn't called")
} else {
req := provider.ValidateProviderConfigRequest
if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
t.Errorf("wrong name in ValidateProviderConfig\ngot: %#v\nwant: %#v", got, want)
}
}
if !provider.ConfigureProviderCalled {
t.Error("ConfigureProvider wasn't called")
} else {
req := provider.ConfigureProviderRequest
if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
t.Errorf("wrong name in ConfigureProvider\ngot: %#v\nwant: %#v", got, want)
}
}
if !provider.CloseCalled {
t.Error("provider wasn't closed")
}
})
}
func TestPlanWithRemovedResource(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
attrs := map[string]interface{}{
"id": "FE1D5830765C",
"input": map[string]interface{}{
"value": "hello",
"type": "string",
},
"output": map[string]interface{}{
"value": nil,
"type": "string",
},
"triggers_replace": nil,
}
attrsJSON, err := json.Marshal(attrs)
if err != nil {
t.Fatal(err)
}
// We want to see that it's adding the extra context for when a provider is
// missing for a resource that's in state and not in config.
expectedDiagnostic := "has resources in state that"
tcs := make(map[string]*string)
tcs["missing-providers"] = &expectedDiagnostic
tcs["valid-providers"] = nil
for name, diag := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", name))
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
// PrevState specifies a state with a resource that is not present in
// the current configuration. This is a common situation when a resource
// is removed from the configuration but still exists in the state.
PrevState: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
Key: addrs.NoKey,
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "main",
},
Key: addrs.NoKey,
},
},
DeposedKey: addrs.NotDeposed,
},
}).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
SchemaVersion: 0,
AttrsJSON: attrsJSON,
Status: states.ObjectReady,
}).
SetProviderAddr(addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"),
})).
Build(),
}
changesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
if diag != nil {
if len(diags) == 0 {
t.Fatalf("expected diagnostics, got none")
}
if !strings.Contains(diags[0].Description().Detail, *diag) {
t.Fatalf("expected diagnostic %q, got %q", *diag, diags[0].Description().Detail)
}
} else if len(diags) > 0 {
t.Fatalf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error())
}
})
}
}
func TestPlanWithSensitivePropagation(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
stackaddrs.AbsComponent{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.Component{Name: "sensitive"},
},
),
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": {
{
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
},
PlannedOutputValues: make(map[string]cty.Value),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("secret"),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: []cty.Path{
cty.GetAttrPath("value"),
},
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "sensitive"},
},
),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"out": cty.StringVal("secret").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "id"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.String),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithSensitivePropagationNested(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input-nested"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("stack.sensitive.component.self"),
),
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": {
{
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
},
PlannedOutputValues: make(map[string]cty.Value),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("secret"),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: []cty.Path{
cty.GetAttrPath("value"),
},
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"out": cty.StringVal("secret").Mark(marks.Sensitive),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "id"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.String),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithForEach(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "input-from-component-list"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "components"}: {
Value: cty.ListVal([]cty.Value{cty.StringVal("one"), cty.StringVal("two"), cty.StringVal("three")}),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above/
}
}
func TestPlanWithCheckableObjects(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "checkable-objects")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "foo"}: {
Value: cty.StringVal("bar"),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Check block assertion failed",
Detail: `value must be 'baz'`,
Subject: &hcl.Range{
Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"),
Start: hcl.Pos{Line: 41, Column: 21, Byte: 716},
End: hcl.Pos{Line: 41, Column: 57, Byte: 752},
},
})
go Plan(ctx, &req, &resp)
gotChanges, gotDiags := collectPlanOutput(changesCh, diagsCh)
if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
// The order of emission for our planned changes is unspecified since it
// depends on how the various goroutines get scheduled, and so we'll
// arbitrarily sort gotChanges lexically by the name of the change type
// so that we have some dependable order to diff against below.
sort.Slice(gotChanges, func(i, j int) bool {
ic := gotChanges[i]
jc := gotChanges[j]
return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc)
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "single"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedInputValues: map[string]plans.DynamicValue{
"foo": mustPlanDynamicValueDynamicType(cty.StringVal("bar")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{"foo": nil},
PlannedOutputValues: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
PlannedCheckResults: &states.CheckResults{
ConfigResults: addrs.MakeMap(
addrs.MakeMapElem[addrs.ConfigCheckable](
addrs.Check{
Name: "value_is_baz",
}.InModule(addrs.RootModule),
&states.CheckResultAggregate{
Status: checks.StatusFail,
ObjectResults: addrs.MakeMap(
addrs.MakeMapElem[addrs.Checkable](
addrs.Check{
Name: "value_is_baz",
}.Absolute(addrs.RootModuleInstance),
&states.CheckResultObject{
Status: checks.StatusFail,
FailureMessages: []string{"value must be 'baz'"},
},
),
),
},
),
addrs.MakeMapElem[addrs.ConfigCheckable](
addrs.InputVariable{
Name: "foo",
}.InModule(addrs.RootModule),
&states.CheckResultAggregate{
Status: checks.StatusPass,
ObjectResults: addrs.MakeMap(
addrs.MakeMapElem[addrs.Checkable](
addrs.InputVariable{
Name: "foo",
}.Absolute(addrs.RootModuleInstance),
&states.CheckResultObject{
Status: checks.StatusPass,
},
),
),
},
),
addrs.MakeMapElem[addrs.ConfigCheckable](
addrs.OutputValue{
Name: "foo",
}.InModule(addrs.RootModule),
&states.CheckResultAggregate{
Status: checks.StatusPass,
ObjectResults: addrs.MakeMap(
addrs.MakeMapElem[addrs.Checkable](
addrs.OutputValue{
Name: "foo",
}.Absolute(addrs.RootModuleInstance),
&states.CheckResultObject{
Status: checks.StatusPass,
},
),
),
},
),
addrs.MakeMapElem[addrs.ConfigCheckable](
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "main",
}.InModule(addrs.RootModule),
&states.CheckResultAggregate{
Status: checks.StatusPass,
ObjectResults: addrs.MakeMap(
addrs.MakeMapElem[addrs.Checkable](
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.CheckResultObject{
Status: checks.StatusPass,
},
),
),
},
),
),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "single"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "main",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"value": cty.StringVal("bar"),
}), stacks_testing_provider.TestingResourceSchema.Body),
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithDeferredResource(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "deferrable-component")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "id"}: {
Value: cty.StringVal("62594ae3"),
},
{Name: "defer"}: {
Value: cty.BoolVal(true),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
PlanComplete: false,
PlanApplyable: false, // We don't have any resources to apply since they're deferred.
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("62594ae3")),
"defer": mustPlanDynamicValueDynamicType(cty.BoolVal(true)),
},
PlannedOutputValues: map[string]cty.Value{},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"defer": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_deferred_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_deferred_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_deferred_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("62594ae3"),
"value": cty.NullVal(cty.String),
"deferred": cty.BoolVal(true),
}), stacks_testing_provider.DeferredResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
Schema: stacks_testing_provider.DeferredResourceSchema,
},
DeferredReason: providers.DeferredReasonResourceConfigUnknown,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "defer"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.BoolVal(true),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "id"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("62594ae3"),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithDeferredComponentForEach(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-for-each"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "components"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above/
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "child"},
},
),
PlanApplyable: true,
PlanComplete: false,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
stackaddrs.AbsComponent{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.Component{
Name: "self",
},
},
),
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlannedOutputValues: map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "child",
},
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
},
Key: addrs.NoKey,
},
},
},
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonDeferredPrereq,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
Key: addrs.WildcardKey,
},
),
PlanApplyable: true, // TODO: Questionable? We only have outputs.
PlanComplete: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
},
PlannedOutputValues: map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
DeferredReason: providers.DeferredReasonDeferredPrereq,
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
Key: addrs.WildcardKey,
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "components"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithDeferredComponentReferences(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-references"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "known_components"}: {
Value: cty.ListVal([]cty.Value{cty.StringVal("known")}),
DefRange: tfdiags.SourceRange{},
},
{Name: "unknown_components"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above.
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "children"},
Key: addrs.WildcardKey,
},
),
PlanApplyable: true, // TODO: Questionable? We only have outputs.
PlanComplete: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlannedOutputValues: map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
stackaddrs.AbsComponent{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.Component{
Name: "self",
},
},
),
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
DeferredReason: providers.DeferredReasonDeferredPrereq,
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "children"},
Key: addrs.WildcardKey,
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
Key: addrs.StringKey("known"),
}),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("known")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlannedOutputValues: map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
Key: addrs.StringKey("known"),
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
},
Key: addrs.NoKey,
},
},
},
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
},
Key: addrs.NoKey,
},
},
PrevRunAddr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
},
Key: addrs.NoKey,
},
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("known"),
}), stacks_testing_provider.TestingResourceSchema.Body),
},
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.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "known_components"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{cty.StringVal("known")}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "unknown_components"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
// This test verifies that if an embedded stack is configured with a for_each value that is unknown / deferred
// that the plan will use the wildcard key for the embedded stack and that the components within are planned with
// unknown values.
func TestPlanWithDeferredEmbeddedStackForEach(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-embedded-stack-for-each"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "stacks"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above/
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
PlanApplyable: false, // Everything is deferred, so nothing to apply.
PlanComplete: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
},
PlannedOutputValues: map[string]cty.Value{},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
DeferredReason: providers.DeferredReasonDeferredPrereq,
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "stacks"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
// This test checks that a stack with an embedded stack with unknown for-each value
// and within the embedded stack a component with a for-each value that is deferred
// will plan successfully.
func TestPlanWithDeferredEmbeddedStackAndComponentForEach(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-embedded-stack-and-component-for-each"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "stacks"}: {
Value: cty.UnknownVal(cty.Map(cty.Set(cty.String))),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above/
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
Key: addrs.WildcardKey,
},
),
PlanApplyable: false, // Everything is deferred, so nothing to apply.
PlanComplete: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
},
PlannedOutputValues: map[string]cty.Value{},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
DeferredReason: providers.DeferredReasonDeferredPrereq,
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
Key: addrs.WildcardKey,
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
}), stacks_testing_provider.TestingResourceSchema.Body),
AfterSensitivePaths: nil,
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "stacks"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Map(cty.Set(cty.String))),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "deferred-component-for-each-from-component-of-invalid-type")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "components"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 1 {
t.Fatalf("expected 1 diagnostic, got %d: %s", len(diags), diags)
}
if diags[0].Severity() != tfdiags.Error {
t.Errorf("expected error diagnostic, got %q", diags[0].Severity())
}
expectedSummary := "Invalid for_each value"
if diags[0].Description().Summary != expectedSummary {
t.Errorf("expected diagnostic with summary %q, got %q", expectedSummary, diags[0].Description().Summary)
}
expectedDetail := "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component."
if diags[0].Description().Detail != expectedDetail {
t.Errorf("expected diagnostic with detail %q, got %q", expectedDetail, diags[0].Description().Detail)
}
}
func TestPlanWithDeferredProviderForEach(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-provider-for-each"))
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)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "providers"}: {
Value: cty.UnknownVal(cty.Set(cty.String)),
DefRange: tfdiags.SourceRange{},
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow() // We reported the diags above
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "known"},
}),
PlanComplete: false,
PlanApplyable: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("primary")),
},
PlannedOutputValues: map[string]cty.Value{},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "known"},
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("primary"),
}), stacks_testing_provider.TestingResourceSchema.Body),
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "unknown"},
Key: addrs.WildcardKey,
}),
PlanComplete: false,
PlanApplyable: false,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("secondary")),
},
PlannedOutputValues: map[string]cty.Value{},
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "unknown"},
Key: addrs.WildcardKey,
},
),
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
},
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
PrevRunAddr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "data",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("secondary"),
}), stacks_testing_provider.TestingResourceSchema.Body),
},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "providers"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanInvalidProvidersFailGracefully(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("invalid-providers"))
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)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
changes, diags := collectPlanOutput(changesCh, diagsCh)
sort.SliceStable(diags, diagnosticSortFunc(diags))
expectDiagnosticsForTest(t, diags,
expectDiagnostic(tfdiags.Error, "Provider configuration is invalid", "Cannot plan changes for this resource because its associated provider configuration is invalid."),
expectDiagnostic(tfdiags.Error, "invalid configuration", "configure_error attribute was set"))
sort.SliceStable(changes, func(i, j int) bool {
return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanTimestamp: fakePlanTimestamp,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
}
if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlanWithStateManipulation(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
tcs := map[string]struct {
state *stackstate.State
store *stacks_testing_provider.ResourceStore
inputs map[string]cty.Value
changes []stackplan.PlannedChange
counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]
expectedWarnings []string
}{
"moved": {
state: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "moved",
"value": "moved",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build(),
changes: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Update,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.after"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.before"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})),
},
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "moved",
"value": "moved",
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.self"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.self"),
Move: 1,
},
}),
},
"cross-type-moved": {
state: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "moved",
"value": "moved",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build(),
changes: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Update,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_deferred_resource.after"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_deferred_resource.after"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.before"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
"deferred": cty.False,
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
"deferred": cty.False,
})),
},
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "moved",
"value": "moved",
"deferred": false,
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.DeferredResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.self"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.self"),
Move: 1,
},
}),
},
"import": {
state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this.
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("imported", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("imported"),
"value": cty.StringVal("imported"),
})).
Build(),
inputs: map[string]cty.Value{
"id": cty.StringVal("imported"),
},
changes: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
// The component is still CREATE even though all the
// instances are NoOps, because the component itself didn't
// exist before even though all the resources might have.
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("imported")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("imported"),
"value": cty.StringVal("imported"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("imported"),
"value": cty.StringVal("imported"),
})),
Importing: &plans.ImportingSrc{
ID: "imported",
},
},
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "imported",
"value": "imported",
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "id",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("imported"),
RequiredOnApply: false,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.self"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.self"),
Import: 1,
},
}),
},
"removed": {
state: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "removed",
"value": "removed",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("removed", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})).
Build(),
changes: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Update,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.resource"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.resource"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Forget,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig,
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "removed",
"value": "removed",
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.self"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.self"),
Forget: 1,
},
}),
expectedWarnings: []string{"Some objects will no longer be managed by Terraform"},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name))
gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]()
ctx = ContextWithHooks(ctx, &stackeval.Hooks{
ReportComponentInstancePlanned: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any {
gotCounts.Put(change.Addr, change)
return span
},
})
inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs))
for name, input := range tc.inputs {
inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{
Value: input,
}
}
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.NewProviderWithData(t, tc.store), nil
},
},
DependencyLocks: *lock,
InputValues: inputs,
ForcePlanTimestamp: &fakePlanTimestamp,
PrevState: tc.state,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
changes, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) > len(tc.expectedWarnings) {
t.Fatalf("had unexpected warnings")
}
for i, diag := range diags {
if diag.Description().Summary != tc.expectedWarnings[i] {
t.Fatalf("expected diagnostic with summary %q, got %q", tc.expectedWarnings[i], diag.Description().Summary)
}
}
sort.SliceStable(changes, func(i, j int) bool {
return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
})
if diff := cmp.Diff(tc.changes, changes, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
wantCounts := tc.counts
for key, elem := range wantCounts.All() {
// First, make sure everything we wanted is present.
if !gotCounts.HasKey(key) {
t.Errorf("wrong counts: wanted %s but didn't get it", key)
}
// And that the values actually match.
got, want := gotCounts.Get(key), elem
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong counts for %s: %s", want.Addr, diff)
}
}
for key := range gotCounts.All() {
// Then, make sure we didn't get anything we didn't want.
if !wantCounts.HasKey(key) {
t.Errorf("wrong counts: got %s but didn't want it", key)
}
}
})
}
}
func TestPlan_plantimestamp_force_timestamp(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-plantimestamp")
forcedPlanTimestamp := "1991-08-25T20:57:08Z"
fakePlanTimestamp, err := time.Parse(time.RFC3339, forcedPlanTimestamp)
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
return map[stackaddrs.InputVariable]ExternalInputValue{}
}(),
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
// The following will fail the test if there are any error
// diagnostics.
reportDiagnosticsForTest(t, diags)
// We also want to fail if there are just warnings, since the
// configurations here are supposed to be totally problem-free.
if len(diags) != 0 {
// reportDiagnosticsForTest already showed the diagnostics in
// the log
t.FailNow()
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "second-self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"value": nil,
},
PlannedInputValues: map[string]plans.DynamicValue{
"value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)),
},
PlannedOutputValues: map[string]cty.Value{
"input": cty.StringVal(forcedPlanTimestamp),
"out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"value": nil,
},
PlannedInputValues: map[string]plans.DynamicValue{
"value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)),
},
PlannedOutputValues: map[string]cty.Value{
"input": cty.StringVal(forcedPlanTimestamp),
"out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "plantimestamp"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal(forcedPlanTimestamp),
},
&stackplan.PlannedChangePlannedTimestamp{PlannedTimestamp: fakePlanTimestamp},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlan_plantimestamp_later_than_when_writing_this_test(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-plantimestamp")
dayOfWritingThisTest := "2024-06-21T06:37:08Z"
dayOfWritingThisTestTime, err := time.Parse(time.RFC3339, dayOfWritingThisTest)
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
return map[stackaddrs.InputVariable]ExternalInputValue{}
}(),
ForcePlanTimestamp: nil, // This is what we want to test
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
changes, diags := collectPlanOutput(changesCh, diagsCh)
output := expectOutput(t, "plantimestamp", changes)
plantimestampValue := output.After
plantimestamp, err := time.Parse(time.RFC3339, plantimestampValue.AsString())
if err != nil {
t.Fatal(err)
}
if plantimestamp.Before(dayOfWritingThisTestTime) {
t.Errorf("expected plantimestamp to be later than %q, got %q", dayOfWritingThisTest, plantimestampValue.AsString())
}
// The following will fail the test if there are any error
// diagnostics.
reportDiagnosticsForTest(t, diags)
// We also want to fail if there are just warnings, since the
// configurations here are supposed to be totally problem-free.
if len(diags) != 0 {
// reportDiagnosticsForTest already showed the diagnostics in
// the log
t.FailNow()
}
}
func TestPlan_DependsOnUpdatesRequirements(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "depends-on"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
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(t), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
{Name: "input"}: {
Value: cty.StringVal("hello, world!"),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, diags)
if len(diags) != 0 {
t.FailNow()
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.first"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlanTimestamp: fakePlanTimestamp,
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, world!"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.second"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.first"),
mustAbsComponent("stack.second.component.self"),
),
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlanTimestamp: fakePlanTimestamp,
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, world!"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("stack.first.component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.first"),
mustAbsComponent("component.empty"),
),
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlanTimestamp: fakePlanTimestamp,
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.first.component.self.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, world!"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("stack.second.component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlanTimestamp: fakePlanTimestamp,
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.second.component.self.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.StringVal("hello, world!"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "empty",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetValEmpty(cty.String),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "input",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("hello, world!"),
},
}
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestPlan_RemovedBlocks(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
tcs := map[string]struct {
source string
initialState *stackstate.State
store *stacks_testing_provider.ResourceStore
inputs map[string]cty.Value
wantPlanChanges []stackplan.PlannedChange
wantPlanDiags []expectedDiagnostic
}{
"unknown removed block with nothing to remove": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
// we have a single component instance in state
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
"removed": cty.UnknownVal(cty.Set(cty.String)),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: true,
PlanApplyable: false, // all changes are no-ops
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "a",
"value": "a",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
"unknown removed block with elements in state": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
// we have a single component instance in state
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetValEmpty(cty.String),
"removed": cty.UnknownVal(cty.Set(cty.String)),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: false, // has deferred changes
PlanApplyable: false, // only deferred changes
Action: plans.Delete,
Mode: plans.DestroyMode,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "a",
"value": "a",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonDeferredPrereq,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetValEmpty(cty.String),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
"unknown component block with element to remove": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"b\"]")).
AddInputVariable("id", cty.StringVal("b")).
AddInputVariable("input", cty.StringVal("b"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "b",
"value": "b",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
AddResource("b", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("b"),
"value": cty.StringVal("b"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.UnknownVal(cty.Set(cty.String)),
"removed": cty.SetVal([]cty.Value{cty.StringVal("b")}),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: false, // has deferred changes
PlanApplyable: false, // only deferred changes
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "a",
"value": "a",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonDeferredPrereq,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"b\"]"),
PlanComplete: true,
PlanApplyable: true,
Action: plans.Delete,
Mode: plans.DestroyMode,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("b")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("b")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("b"),
"value": cty.StringVal("b"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "b",
"value": "b",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{cty.StringVal("b")}),
},
},
},
"unknown component and removed block with element in state": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.UnknownVal(cty.Set(cty.String)),
"removed": cty.UnknownVal(cty.Set(cty.String)),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: false, // has deferred changes
PlanApplyable: false, // only deferred changes
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "a",
"value": "a",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
DeferredReason: providers.DeferredReasonDeferredPrereq,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
"absent component": {
source: filepath.Join("with-single-input", "removed-component"),
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
},
"absent component instance": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
"removed": cty.SetVal([]cty.Value{
cty.StringVal("b"), // Doesn't exist!
}),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
// we're expecting the new component to be created
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: true,
PlanApplyable: false, // no changes
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstanceRemoved{
Addr: mustAbsComponentInstance("component.self[\"removed\"]"),
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("b"),
}),
},
},
},
"orphaned component": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "removed",
"value": "removed",
}),
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"orphaned\"]")).
AddInputVariable("id", cty.StringVal("orphaned")).
AddInputVariable("input", cty.StringVal("orphaned"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"orphaned\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "orphaned",
"value": "orphaned",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("removed", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})).
AddResource("orphaned", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("orphaned"),
"value": cty.StringVal("orphaned"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
"removed": cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: false, // No! We have an unclaimed instance!
},
// we're expecting the new component to be created
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"added\"]"),
PlanComplete: true,
PlanApplyable: true,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("added")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("added")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("added"),
"value": cty.StringVal("added"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"removed\"]"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Delete,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "removed",
"value": "removed",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
},
wantPlanDiags: []expectedDiagnostic{
{
severity: tfdiags.Error,
summary: "Unclaimed component instance",
detail: "The component instance component.self[\"orphaned\"] is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.",
},
},
},
"duplicate component": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
"removed": cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: false, // No! The removed block is a duplicate of the component!
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: true,
PlanApplyable: false, // no changes
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
},
wantPlanDiags: []expectedDiagnostic{
{
severity: tfdiags.Error,
summary: "Cannot remove component instance",
detail: "The component instance component.self[\"a\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl:18,1-17.",
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, tc.source)
inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs))
for name, input := range tc.inputs {
inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{
Value: input,
}
}
providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, tc.store), nil
},
}
planChangesCh := make(chan stackplan.PlannedChange)
planDiagsCh := make(chan tfdiags.Diagnostic)
planReq := PlanRequest{
Config: cfg,
ProviderFactories: providers,
InputValues: inputs,
ForcePlanTimestamp: &fakePlanTimestamp,
PrevState: tc.initialState,
DependencyLocks: *lock,
}
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: planDiagsCh,
}
go Plan(ctx, &planReq, &planResp)
gotPlanChanges, gotPlanDiags := collectPlanOutput(planChangesCh, planDiagsCh)
sort.SliceStable(gotPlanChanges, func(i, j int) bool {
return plannedChangeSortKey(gotPlanChanges[i]) < plannedChangeSortKey(gotPlanChanges[j])
})
sort.SliceStable(gotPlanDiags, diagnosticSortFunc(gotPlanDiags))
expectDiagnosticsForTest(t, gotPlanDiags, tc.wantPlanDiags...)
if diff := cmp.Diff(tc.wantPlanChanges, gotPlanChanges, ctydebug.CmpOptions, cmpCollectionsSet, cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{})); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
})
}
}
func TestPlanWithResourceIdentities(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "resource-identity")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ForcePlanTimestamp: &fakePlanTimestamp,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
wantChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: stackaddrs.Absolute(
stackaddrs.RootStackInstance,
stackaddrs.ComponentInstance{
Component: stackaddrs.Component{Name: "self"},
},
),
Action: plans.Create,
PlanApplyable: true,
PlanComplete: true,
PlannedCheckResults: &states.CheckResults{},
PlannedInputValues: map[string]plans.DynamicValue{
"name": mustPlanDynamicValueDynamicType(cty.StringVal("example")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{"name": nil},
PlannedOutputValues: map[string]cty.Value{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource_with_identity.hello"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource_with_identity.hello"),
PrevRunAddr: mustAbsResourceInstance("testing_resource_with_identity.hello"),
ProviderAddr: mustDefaultRootProvider("testing"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("example"),
"value": cty.NullVal(cty.String),
})),
AfterIdentity: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("id:example"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceWithIdentitySchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
})
if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
// collectPlanOutput consumes the two output channels emitting results from
// a call to [Plan], and collects all of the data written to them before
// returning once changesCh has been closed by the sender to indicate that
// the planning process is complete.
func collectPlanOutput(changesCh <-chan stackplan.PlannedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
var changes []stackplan.PlannedChange
var diags tfdiags.Diagnostics
for {
select {
case change, ok := <-changesCh:
if !ok {
// The plan operation is complete but we might still have
// some buffered diagnostics to consume.
if diagsCh != nil {
for diag := range diagsCh {
diags = append(diags, diag)
}
}
return changes, diags
}
changes = append(changes, change)
case diag, ok := <-diagsCh:
if !ok {
// no more diagnostics to read
diagsCh = nil
continue
}
diags = append(diags, diag)
}
}
}
func expectOutput(t *testing.T, name string, changes []stackplan.PlannedChange) *stackplan.PlannedChangeOutputValue {
t.Helper()
for _, change := range changes {
if v, ok := change.(*stackplan.PlannedChangeOutputValue); ok && v.Addr.Name == name {
return v
}
}
t.Fatalf("expected output value %q", name)
return nil
}
var cmpCollectionsSet = cmp.Comparer(func(x, y collections.Set[stackaddrs.AbsComponent]) bool {
if x.Len() != y.Len() {
return false
}
for v := range x.All() {
if !y.Has(v) {
return false
}
}
return true
})