mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
This helps us catch diagnostics in locals that only happen during evaluation Co-authored-by: Mutahhir Hayat <mutahhir.hayat@hashicorp.com> Co-authored-by: Matej Risek <matej.risek@hashicorp.com>
6392 lines
226 KiB
Go
6392 lines
226 KiB
Go
// 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/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)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"removed embedded component duplicate": {
|
|
path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"for_each_input": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
"simple_input": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
"for_each_removed": cty.SetVal([]cty.Value{
|
|
cty.StringVal("foo"),
|
|
}),
|
|
"simple_removed": cty.SetVal([]cty.Value{
|
|
cty.StringVal("foo"),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot remove component instance",
|
|
Detail: "The component instance stack.for_each.component.self[\"foo\"] 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/for-each-component/for-each-component.tfcomponent.hcl:15,1-17.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 38, Column: 1, Byte: 505},
|
|
End: hcl.Pos{Line: 38, Column: 8, Byte: 512},
|
|
},
|
|
})
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot remove component instance",
|
|
Detail: "The component instance stack.simple[\"foo\"].component.self 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/valid/valid.tfcomponent.hcl:19,1-17.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 60, Column: 1, Byte: 811},
|
|
End: hcl.Pos{Line: 60, Column: 8, Byte: 818},
|
|
},
|
|
})
|
|
return diags
|
|
}),
|
|
},
|
|
},
|
|
"deferred-embedded-stack-update": {
|
|
path: path.Join("with-single-input", "deferred-embedded-stack-for-each"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a[\"deferred\"].component.self")).
|
|
AddInputVariable("id", cty.StringVal("deferred")).
|
|
AddInputVariable("input", cty.StringVal("deferred"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.a[\"deferred\"].component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "deferred",
|
|
"value": "deferred",
|
|
}),
|
|
})).
|
|
AddInput("stacks", cty.MapVal(map[string]cty.Value{
|
|
"deferred": cty.StringVal("deferred"),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("deferred", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("deferred"),
|
|
"value": cty.StringVal("deferred"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"stacks": cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
wantPlannedChanges: []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.StringKey("deferred")),
|
|
stackaddrs.ComponentInstance{
|
|
Component: stackaddrs.Component{Name: "self"},
|
|
},
|
|
),
|
|
PlanApplyable: false, // Everything is deferred, so nothing to apply.
|
|
PlanComplete: false,
|
|
Action: plans.Update,
|
|
PlannedInputValues: map[string]plans.DynamicValue{
|
|
"id": mustPlanDynamicValueDynamicType(cty.StringVal("deferred")),
|
|
"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.StringKey("deferred")),
|
|
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.Update,
|
|
Before: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("deferred"),
|
|
"value": cty.StringVal("deferred"),
|
|
}), stacks_testing_provider.TestingResourceSchema.Body),
|
|
After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("deferred"),
|
|
"value": cty.UnknownVal(cty.String),
|
|
}), stacks_testing_provider.TestingResourceSchema.Body),
|
|
AfterSensitivePaths: nil,
|
|
},
|
|
},
|
|
PriorStateSrc: &states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "deferred",
|
|
"value": "deferred",
|
|
}),
|
|
Dependencies: make([]addrs.ConfigResource, 0),
|
|
},
|
|
Schema: stacks_testing_provider.TestingResourceSchema,
|
|
},
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "stacks"},
|
|
Action: plans.Update,
|
|
Before: cty.MapVal(map[string]cty.Value{
|
|
"deferred": cty.StringVal("deferred"),
|
|
}),
|
|
After: cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"deferred-embedded-stack-create": {
|
|
path: path.Join("with-single-input", "deferred-embedded-stack-for-each"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"stacks": cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
wantPlannedChanges: []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.UnknownVal(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.Map(cty.String)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"deferred-embedded-stack-and-component-for-each": {
|
|
path: path.Join("with-single-input", "deferred-embedded-stack-and-component-for-each"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"stacks": cty.UnknownVal(cty.Map(cty.Set(cty.String))),
|
|
},
|
|
wantPlannedChanges: []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))),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"removed block targets stack not in configuration or state": {
|
|
path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"input": cty.MapValEmpty(cty.String),
|
|
"removed": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
},
|
|
wantPlannedChanges: []stackplan.PlannedChange{
|
|
&stackplan.PlannedChangeApplyable{
|
|
Applyable: true,
|
|
},
|
|
&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.MapValEmpty(cty.String),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "removed"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "removed-direct"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.SetValEmpty(cty.String),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"embedded stack in state but not in configuration": {
|
|
path: filepath.Join("with-single-input", "valid"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.child.component.self"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.child.component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "leftover",
|
|
"value": "leftover",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("leftover", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("leftover"),
|
|
"value": cty.StringVal("leftover"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"input": cty.StringVal("input"),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unclaimed component instance",
|
|
Detail: "The component instance stack.child.component.self 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.",
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"removed and stack block target the same stack": {
|
|
path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"input": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
"removed": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot remove stack instance",
|
|
Detail: "The stack instance stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 36, Column: 1, Byte: 441},
|
|
End: hcl.Pos{Line: 36, Column: 8, Byte: 448},
|
|
},
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"removed targets stack block in embedded stack that exists": {
|
|
path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"input": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
"removed": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.MapVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"input": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot remove stack instance",
|
|
Detail: "The stack instance stack.embedded[\"component\"].stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 28, Column: 1, Byte: 360},
|
|
End: hcl.Pos{Line: 28, Column: 8, Byte: 367},
|
|
},
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"removed block targets component inside removed stack": {
|
|
path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
|
|
AddInputVariable("id", cty.StringVal("component")).
|
|
AddInputVariable("input", cty.StringVal("component"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("component", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"removed": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
"removed-direct": cty.SetVal([]cty.Value{
|
|
cty.StringVal("component"),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot remove component instance",
|
|
Detail: "The component instance stack.simple[\"component\"].component.self 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/valid/valid.tfcomponent.hcl:19,1-17.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 51, Column: 1, Byte: 708},
|
|
End: hcl.Pos{Line: 51, Column: 8, Byte: 715},
|
|
},
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"removed block targets orphaned component": {
|
|
path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
|
|
AddInputVariable("id", cty.StringVal("component")).
|
|
AddInputVariable("input", cty.StringVal("component"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("component", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"simple_input": cty.MapValEmpty(cty.String),
|
|
"simple_removed": cty.SetVal([]cty.Value{
|
|
cty.StringVal("component"),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid removed block",
|
|
Detail: "The component instance stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 60, Column: 1, Byte: 811},
|
|
End: hcl.Pos{Line: 60, Column: 8, Byte: 818},
|
|
},
|
|
})
|
|
}),
|
|
wantPlannedChanges: []stackplan.PlannedChange{
|
|
&stackplan.PlannedChangeApplyable{
|
|
Applyable: false,
|
|
},
|
|
&stackplan.PlannedChangeHeader{
|
|
TerraformVersion: version.SemVer,
|
|
},
|
|
&stackplan.PlannedChangePlannedTimestamp{
|
|
PlannedTimestamp: fakePlanTimestamp,
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "for_each_input"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.MapValEmpty(cty.String),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "for_each_removed"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.SetValEmpty(cty.String),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "simple_input"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.MapValEmpty(cty.String),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "simple_removed"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.SetVal([]cty.Value{
|
|
cty.StringVal("component"),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"removed block targets orphaned stack": {
|
|
path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded[\"component\"].stack.simple[\"component\"].component.self")).
|
|
AddInputVariable("id", cty.StringVal("component")).
|
|
AddInputVariable("input", cty.StringVal("component"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.embedded[\"component\"].stack.simple[\"component\"].component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("component", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"input": cty.MapValEmpty(cty.Map(cty.String)),
|
|
"removed": cty.MapVal(map[string]cty.Value{
|
|
"component": cty.MapVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"input": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
},
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid removed block",
|
|
Detail: "The component instance stack.embedded[\"component\"].stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 28, Column: 1, Byte: 360},
|
|
End: hcl.Pos{Line: 28, Column: 8, Byte: 367},
|
|
},
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"removed block targets orphaned component without config definition": {
|
|
path: filepath.Join("with-single-input", "orphaned-component"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded.component.self")).
|
|
AddInputVariable("id", cty.StringVal("component")).
|
|
AddInputVariable("input", cty.StringVal("component"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.embedded.component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("component", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
|
return diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid removed block",
|
|
Detail: "The component instance stack.embedded.component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//with-single-input/orphaned-component/orphaned-component.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 10, Column: 1, Byte: 131},
|
|
End: hcl.Pos{Line: 10, Column: 8, Byte: 138},
|
|
},
|
|
})
|
|
}),
|
|
},
|
|
},
|
|
"unknown embedded stack with internal component targeted by concrete removed block": {
|
|
path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
|
|
AddInputVariable("id", cty.StringVal("component")).
|
|
AddInputVariable("input", cty.StringVal("component"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("component", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
planInputs: map[string]cty.Value{
|
|
"removed": cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
wantPlannedChanges: []stackplan.PlannedChange{
|
|
&stackplan.PlannedChangeApplyable{
|
|
Applyable: true,
|
|
},
|
|
&stackplan.PlannedChangeHeader{
|
|
TerraformVersion: version.SemVer,
|
|
},
|
|
&stackplan.PlannedChangePlannedTimestamp{
|
|
PlannedTimestamp: fakePlanTimestamp,
|
|
},
|
|
&stackplan.PlannedChangeComponentInstance{
|
|
Addr: mustAbsComponentInstance("stack.simple[\"component\"].component.self"),
|
|
Action: plans.Delete,
|
|
Mode: plans.DestroyMode,
|
|
PlannedInputValues: map[string]plans.DynamicValue{
|
|
"id": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
|
|
"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
|
|
},
|
|
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
|
|
"id": nil,
|
|
"input": nil,
|
|
},
|
|
PlannedOutputValues: make(map[string]cty.Value),
|
|
PlannedCheckResults: &states.CheckResults{},
|
|
PlanTimestamp: fakePlanTimestamp,
|
|
},
|
|
&stackplan.PlannedChangeDeferredResourceInstancePlanned{
|
|
ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
|
|
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"component\"].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.Delete,
|
|
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("component"),
|
|
"value": cty.StringVal("component"),
|
|
})),
|
|
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"value": cty.String,
|
|
}))),
|
|
},
|
|
},
|
|
PriorStateSrc: &states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "component",
|
|
"value": "component",
|
|
}),
|
|
Dependencies: make([]addrs.ConfigResource, 0),
|
|
},
|
|
ProviderConfigAddr: mustDefaultRootProvider("testing"),
|
|
Schema: stacks_testing_provider.TestingResourceSchema,
|
|
},
|
|
DeferredReason: providers.DeferredReasonDeferredPrereq,
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "input"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.MapValEmpty(cty.String),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "removed"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
&stackplan.PlannedChangeRootInputValue{
|
|
Addr: stackaddrs.InputVariable{Name: "removed-direct"},
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(cty.DynamicPseudoType),
|
|
After: cty.SetValEmpty(cty.String),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"remove partial stack": {
|
|
path: filepath.Join("with-single-input", "multiple-components", "removed"),
|
|
state: stackstate.NewStateBuilder().
|
|
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.multiple.component.one")).
|
|
AddInputVariable("id", cty.StringVal("one")).
|
|
AddInputVariable("input", cty.StringVal("one"))).
|
|
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
|
|
SetAddr(mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data")).
|
|
SetProviderAddr(mustDefaultRootProvider("testing")).
|
|
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "one",
|
|
"value": "one",
|
|
}),
|
|
})).
|
|
Build(),
|
|
store: stacks_testing_provider.NewResourceStoreBuilder().
|
|
AddResource("one", cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("one"),
|
|
"value": cty.StringVal("one"),
|
|
})).
|
|
Build(),
|
|
cycle: TestCycle{
|
|
wantPlannedChanges: []stackplan.PlannedChange{
|
|
&stackplan.PlannedChangeApplyable{
|
|
Applyable: true,
|
|
},
|
|
&stackplan.PlannedChangeHeader{
|
|
TerraformVersion: version.SemVer,
|
|
},
|
|
&stackplan.PlannedChangePlannedTimestamp{
|
|
PlannedTimestamp: fakePlanTimestamp,
|
|
},
|
|
&stackplan.PlannedChangeComponentInstance{
|
|
Addr: mustAbsComponentInstance("stack.multiple.component.one"),
|
|
PlanApplyable: true,
|
|
PlanComplete: true,
|
|
Action: plans.Delete,
|
|
Mode: plans.DestroyMode,
|
|
PlannedInputValues: map[string]plans.DynamicValue{
|
|
"id": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
|
|
"input": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
|
|
},
|
|
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
|
|
"id": nil,
|
|
"input": nil,
|
|
},
|
|
PlannedOutputValues: make(map[string]cty.Value),
|
|
PlannedCheckResults: &states.CheckResults{},
|
|
PlanTimestamp: fakePlanTimestamp,
|
|
},
|
|
&stackplan.PlannedChangeResourceInstancePlanned{
|
|
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data"),
|
|
ChangeSrc: &plans.ResourceInstanceChangeSrc{
|
|
Addr: mustAbsResourceInstance("testing_resource.data"),
|
|
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
|
|
ProviderAddr: mustDefaultRootProvider("testing"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Delete,
|
|
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("one"),
|
|
"value": cty.StringVal("one"),
|
|
})),
|
|
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"value": cty.String,
|
|
}))),
|
|
},
|
|
},
|
|
PriorStateSrc: &states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
|
|
"id": "one",
|
|
"value": "one",
|
|
}),
|
|
Dependencies: make([]addrs.ConfigResource, 0),
|
|
},
|
|
ProviderConfigAddr: mustDefaultRootProvider("testing"),
|
|
Schema: stacks_testing_provider.TestingResourceSchema,
|
|
},
|
|
&stackplan.PlannedChangeComponentInstance{
|
|
Addr: mustAbsComponentInstance("stack.multiple.component.two"),
|
|
PlanApplyable: true,
|
|
PlanComplete: true,
|
|
Action: plans.Delete,
|
|
Mode: plans.DestroyMode,
|
|
PlannedOutputValues: make(map[string]cty.Value),
|
|
PlanTimestamp: fakePlanTimestamp,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
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.tfcomponent.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.tfcomponent.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.tfcomponent.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)
|
|
}
|
|
}
|
|
|
|
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.tfcomponent.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, changesCmpOpts); 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)
|
|
}
|
|
}
|
|
|
|
func TestPlanInvalidLocalValue(t *testing.T) {
|
|
ctx := context.Background()
|
|
cfg := loadMainBundleConfigForTest(t, "invalid-local")
|
|
|
|
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,
|
|
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
|
|
{Name: "in"}: {
|
|
Value: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("foo")}),
|
|
},
|
|
},
|
|
}
|
|
resp := PlanResponse{
|
|
PlannedChanges: changesCh,
|
|
Diagnostics: diagsCh,
|
|
}
|
|
|
|
go Plan(ctx, &req, &resp)
|
|
gotChanges, diags := collectPlanOutput(changesCh, diagsCh)
|
|
|
|
tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid operand",
|
|
Detail: "Unsuitable value for left operand: a number is required.",
|
|
Subject: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 19, Column: 49, Byte: 377},
|
|
End: hcl.Pos{Line: 19, Column: 50, Byte: 378},
|
|
},
|
|
Context: &hcl.Range{
|
|
Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl",
|
|
Start: hcl.Pos{Line: 19, Column: 49, Byte: 377},
|
|
End: hcl.Pos{Line: 19, Column: 54, Byte: 382},
|
|
},
|
|
}))
|
|
|
|
// We don't really care about the precise content of the plan changes here,
|
|
// we just want to ensure that the produced plan is not applyable
|
|
sort.SliceStable(gotChanges, func(i, j int) bool {
|
|
return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
|
|
})
|
|
|
|
pca, ok := gotChanges[0].(*stackplan.PlannedChangeApplyable)
|
|
if !ok {
|
|
t.Fatalf("expected first change to be PlannedChangeApplyable, got %T", gotChanges[0])
|
|
}
|
|
if pca.Applyable {
|
|
t.Fatalf("expected plan to be not applyable due to invalid local value, but it is applyable")
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|